import { Prisma } from '@prisma/client' import bcrypt from 'bcryptjs' import { GraphQLError, GraphQLScalarType, Kind } from 'graphql' import jwt from 'jsonwebtoken' import { prisma } from '@/lib/prisma' import { notifyMany, notifyOrganization } from '@/lib/realtime' import { DaDataService } from '@/services/dadata-service' import { MarketplaceService } from '@/services/marketplace-service' import { SmsService } from '@/services/sms-service' import { WildberriesService } from '@/services/wildberries-service' import '@/lib/seed-init' // Автоматическая инициализация БД // Импорт новых resolvers для системы поставок v2 import { fulfillmentConsumableV2Queries, fulfillmentConsumableV2Mutations } from './resolvers/fulfillment-consumables-v2' // 🔒 СИСТЕМА БЕЗОПАСНОСТИ - импорты import { CommercialDataAudit } from './security/commercial-data-audit' import { createSecurityContext } from './security/index' // 🔒 HELPER: Создание безопасного контекста с организационными данными function createSecureContextWithOrgData(context: Context, currentUser: any) { return { ...context, user: { ...context.user, organizationType: currentUser.organization.type, organizationId: currentUser.organization.id, }, } } import { ParticipantIsolation } from './security/participant-isolation' import { SupplyDataFilter } from './security/supply-data-filter' import type { SecurityContext } from './security/types' // Сервисы const smsService = new SmsService() const dadataService = new DaDataService() const marketplaceService = new MarketplaceService() // Функция генерации уникального реферального кода const generateReferralCode = async (): Promise => { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' let attempts = 0 const maxAttempts = 10 while (attempts < maxAttempts) { let code = '' for (let i = 0; i < 10; i++) { code += chars.charAt(Math.floor(Math.random() * chars.length)) } // Проверяем уникальность const existing = await prisma.organization.findUnique({ where: { referralCode: code }, }) if (!existing) { return code } attempts++ } // Если не удалось сгенерировать уникальный код, используем cuid как fallback return `REF${Date.now()}${Math.random().toString(36).substr(2, 5).toUpperCase()}` } // Функция для автоматического создания записи склада при новом партнерстве const autoCreateWarehouseEntry = async (sellerId: string, fulfillmentId: string) => { console.warn(`🏗️ AUTO WAREHOUSE ENTRY: Creating for seller ${sellerId} with fulfillment ${fulfillmentId}`) // Получаем данные селлера const sellerOrg = await prisma.organization.findUnique({ where: { id: sellerId }, }) if (!sellerOrg) { throw new Error(`Селлер с ID ${sellerId} не найден`) } // Проверяем что не существует уже записи для этого селлера у этого фулфилмента // В будущем здесь может быть проверка в отдельной таблице warehouse_entries // Пока используем логику проверки через контрагентов // ЛОГИКА ОПРЕДЕЛЕНИЯ НАЗВАНИЯ МАГАЗИНА (консистентно с warehouseData resolver) let storeName = sellerOrg.name if (sellerOrg.fullName && sellerOrg.name?.includes('ИП')) { // Извлекаем название из скобок, например: "ИП Антипова Д. В. (Renrel)" -> "Renrel" const match = sellerOrg.fullName.match(/\(([^)]+)\)/) if (match && match[1]) { storeName = match[1] } } // Создаем структуру данных для склада const warehouseEntry = { id: `warehouse_${sellerId}_${Date.now()}`, // Уникальный ID записи storeName: storeName || sellerOrg.fullName || sellerOrg.name, storeOwner: sellerOrg.inn || sellerOrg.fullName || sellerOrg.name, storeImage: sellerOrg.logoUrl || null, storeQuantity: 0, // Пока нет поставок partnershipDate: new Date(), products: [], // Пустой массив продуктов } console.warn('✅ AUTO WAREHOUSE ENTRY CREATED:', { sellerId, storeName: warehouseEntry.storeName, storeOwner: warehouseEntry.storeOwner, }) // В реальной системе здесь бы была запись в таблицу warehouse_entries // Пока возвращаем структуру данных return warehouseEntry } // Интерфейсы для типизации interface Context { user?: { id: string phone: string } admin?: { id: string username: string } } interface CreateEmployeeInput { firstName: string lastName: string middleName?: string birthDate?: string avatar?: string passportPhoto?: string passportSeries?: string passportNumber?: string passportIssued?: string passportDate?: string address?: string position: string department?: string hireDate: string salary?: number phone: string email?: string telegram?: string whatsapp?: string emergencyContact?: string emergencyPhone?: string } interface UpdateEmployeeInput { firstName?: string lastName?: string middleName?: string birthDate?: string avatar?: string passportPhoto?: string passportSeries?: string passportNumber?: string passportIssued?: string passportDate?: string address?: string position?: string department?: string hireDate?: string salary?: number status?: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED' phone?: string email?: string telegram?: string whatsapp?: string emergencyContact?: string emergencyPhone?: string } interface UpdateScheduleInput { employeeId: string date: string status: 'WORK' | 'WEEKEND' | 'VACATION' | 'SICK' | 'ABSENT' hoursWorked?: number overtimeHours?: number notes?: string } interface AuthTokenPayload { userId: string phone: string } // JWT утилиты const generateToken = (payload: AuthTokenPayload): string => { return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '30d' }) } // eslint-disable-next-line @typescript-eslint/no-unused-vars const verifyToken = (token: string): AuthTokenPayload => { try { return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { throw new GraphQLError('Недействительный токен', { extensions: { code: 'UNAUTHENTICATED' }, }) } } // Скалярный тип для JSON const JSONScalar = new GraphQLScalarType({ name: 'JSON', description: 'JSON custom scalar type', serialize(value: unknown) { return value // значение отправляется клиенту }, parseValue(value: unknown) { return value // значение получено от клиента }, parseLiteral(ast) { switch (ast.kind) { case Kind.STRING: case Kind.BOOLEAN: return ast.value case Kind.INT: case Kind.FLOAT: return parseFloat(ast.value) case Kind.OBJECT: { const value = Object.create(null) ast.fields.forEach((field) => { value[field.name.value] = parseLiteral(field.value) }) return value } case Kind.LIST: return ast.values.map(parseLiteral) default: return null } }, }) // Скалярный тип для DateTime const DateTimeScalar = new GraphQLScalarType({ name: 'DateTime', description: 'DateTime custom scalar type', serialize(value: unknown) { if (value instanceof Date) { return value.toISOString() // значение отправляется клиенту как ISO строка } return value }, parseValue(value: unknown) { if (typeof value === 'string') { return new Date(value) // значение получено от клиента, парсим как дату } return value }, parseLiteral(ast) { if (ast.kind === Kind.STRING) { return new Date(ast.value) // AST значение как дата } return null }, }) function parseLiteral(ast: unknown): unknown { const astNode = ast as { kind: string value?: unknown fields?: unknown[] values?: unknown[] } switch (astNode.kind) { case Kind.STRING: case Kind.BOOLEAN: return astNode.value case Kind.INT: case Kind.FLOAT: return parseFloat(astNode.value as string) case Kind.OBJECT: { const value = Object.create(null) if (astNode.fields) { astNode.fields.forEach((field: unknown) => { const fieldNode = field as { name: { value: string } value: unknown } value[fieldNode.name.value] = parseLiteral(fieldNode.value) }) } return value } case Kind.LIST: return (ast as { values: unknown[] }).values.map(parseLiteral) default: return null } } export const resolvers = { JSON: JSONScalar, DateTime: DateTimeScalar, Query: { me: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } return await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }) }, organization: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const organization = await prisma.organization.findUnique({ where: { id: args.id }, include: { apiKeys: true, users: true, }, }) if (!organization) { throw new GraphQLError('Организация не найдена') } // Проверяем, что пользователь имеет доступ к этой организации const hasAccess = organization.users.some((user) => user.id === context.user!.id) if (!hasAccess) { throw new GraphQLError('Нет доступа к этой организации', { extensions: { code: 'FORBIDDEN' }, }) } return organization }, // Поиск организаций по типу для добавления в контрагенты searchOrganizations: async (_: unknown, args: { type?: string; search?: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } // Получаем текущую организацию пользователя const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Получаем уже существующих контрагентов для добавления флага const existingCounterparties = await prisma.counterparty.findMany({ where: { organizationId: currentUser.organization.id }, select: { counterpartyId: true }, }) const existingCounterpartyIds = existingCounterparties.map((c) => c.counterpartyId) // Получаем исходящие заявки для добавления флага hasOutgoingRequest const outgoingRequests = await prisma.counterpartyRequest.findMany({ where: { senderId: currentUser.organization.id, status: 'PENDING', }, select: { receiverId: true }, }) const outgoingRequestIds = outgoingRequests.map((r) => r.receiverId) // Получаем входящие заявки для добавления флага hasIncomingRequest const incomingRequests = await prisma.counterpartyRequest.findMany({ where: { receiverId: currentUser.organization.id, status: 'PENDING', }, select: { senderId: true }, }) const incomingRequestIds = incomingRequests.map((r) => r.senderId) const where: Record = { // Больше не исключаем собственную организацию } if (args.type) { where.type = args.type } if (args.search) { where.OR = [ { name: { contains: args.search, mode: 'insensitive' } }, { fullName: { contains: args.search, mode: 'insensitive' } }, { inn: { contains: args.search } }, ] } const organizations = await prisma.organization.findMany({ where, take: 50, // Ограничиваем количество результатов orderBy: { createdAt: 'desc' }, include: { users: true, apiKeys: true, }, }) // Добавляем флаги isCounterparty, isCurrentUser, hasOutgoingRequest и hasIncomingRequest к каждой организации return organizations.map((org) => ({ ...org, isCounterparty: existingCounterpartyIds.includes(org.id), isCurrentUser: org.id === currentUser.organization?.id, hasOutgoingRequest: outgoingRequestIds.includes(org.id), hasIncomingRequest: incomingRequestIds.includes(org.id), })) }, // Мои контрагенты myCounterparties: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } const counterparties = await prisma.counterparty.findMany({ where: { organizationId: currentUser.organization.id }, include: { counterparty: { include: { users: true, apiKeys: true, }, }, }, }) return counterparties.map((c) => c.counterparty) }, // Поставщики поставок supplySuppliers: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } const suppliers = await prisma.supplySupplier.findMany({ where: { organizationId: currentUser.organization.id }, orderBy: { createdAt: 'desc' }, }) return suppliers }, // Логистика конкретной организации organizationLogistics: async (_: unknown, args: { organizationId: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } return await prisma.logistics.findMany({ where: { organizationId: args.organizationId }, orderBy: { createdAt: 'desc' }, }) }, // Входящие заявки incomingRequests: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } return await prisma.counterpartyRequest.findMany({ where: { receiverId: currentUser.organization.id, status: 'PENDING', }, include: { sender: { include: { users: true, apiKeys: true, }, }, receiver: { include: { users: true, apiKeys: true, }, }, }, orderBy: { createdAt: 'desc' }, }) }, // Исходящие заявки outgoingRequests: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } return await prisma.counterpartyRequest.findMany({ where: { senderId: currentUser.organization.id, status: { in: ['PENDING', 'REJECTED'] }, }, include: { sender: { include: { users: true, apiKeys: true, }, }, receiver: { include: { users: true, apiKeys: true, }, }, }, orderBy: { createdAt: 'desc' }, }) }, // Сообщения с контрагентом messages: async ( _: unknown, args: { counterpartyId: string; limit?: number; offset?: number }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } const limit = args.limit || 50 const offset = args.offset || 0 const messages = await prisma.message.findMany({ where: { OR: [ { senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.counterpartyId, }, { senderOrganizationId: args.counterpartyId, receiverOrganizationId: currentUser.organization.id, }, ], }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, orderBy: { createdAt: 'asc' }, take: limit, skip: offset, }) return messages }, // Список чатов (последние сообщения с каждым контрагентом) conversations: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Получаем всех контрагентов const counterparties = await prisma.counterparty.findMany({ where: { organizationId: currentUser.organization.id }, include: { counterparty: { include: { users: true, }, }, }, }) // Для каждого контрагента получаем последнее сообщение и количество непрочитанных const conversations = await Promise.all( counterparties.map(async (cp) => { const counterpartyId = cp.counterparty.id // Последнее сообщение с этим контрагентом const lastMessage = await prisma.message.findFirst({ where: { OR: [ { senderOrganizationId: currentUser.organization!.id, receiverOrganizationId: counterpartyId, }, { senderOrganizationId: counterpartyId, receiverOrganizationId: currentUser.organization!.id, }, ], }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, orderBy: { createdAt: 'desc' }, }) // Количество непрочитанных сообщений от этого контрагента const unreadCount = await prisma.message.count({ where: { senderOrganizationId: counterpartyId, receiverOrganizationId: currentUser.organization!.id, isRead: false, }, }) // Если есть сообщения с этим контрагентом, включаем его в список if (lastMessage) { return { id: `${currentUser.organization!.id}-${counterpartyId}`, counterparty: cp.counterparty, lastMessage, unreadCount, updatedAt: lastMessage.createdAt, } } return null }), ) // Фильтруем null значения и сортируем по времени последнего сообщения return conversations .filter((conv) => conv !== null) .sort((a, b) => new Date(b!.updatedAt).getTime() - new Date(a!.updatedAt).getTime()) }, // Мои услуги myServices: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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('Услуги доступны только для фулфилмент центров') } return await prisma.service.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true }, orderBy: { createdAt: 'desc' }, }) }, // Расходники селлеров (материалы клиентов на складе фулфилмента) mySupplies: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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') { return [] // Только фулфилменты имеют расходники } // Получаем ВСЕ расходники из таблицы supply для фулфилмента const allSupplies = await prisma.supply.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true }, orderBy: { createdAt: 'desc' }, }) // Преобразуем старую структуру в новую согласно GraphQL схеме const transformedSupplies = allSupplies.map((supply) => ({ id: supply.id, name: supply.name, description: supply.description, pricePerUnit: supply.price ? parseFloat(supply.price.toString()) : null, // Конвертируем Decimal в Number unit: supply.unit || 'шт', // Единица измерения imageUrl: supply.imageUrl, warehouseStock: supply.currentStock || 0, // Остаток на складе isAvailable: (supply.currentStock || 0) > 0, // Есть ли в наличии warehouseConsumableId: supply.id, // Связь со складом (пока используем тот же ID) createdAt: supply.createdAt, updatedAt: supply.updatedAt, organization: supply.organization, })) console.warn('🔥 SUPPLIES RESOLVER - NEW FORMAT:', { organizationId: currentUser.organization.id, suppliesCount: transformedSupplies.length, supplies: transformedSupplies.map((s) => ({ id: s.id, name: s.name, pricePerUnit: s.pricePerUnit, warehouseStock: s.warehouseStock, isAvailable: s.isAvailable, })), }) return transformedSupplies }, // Доступные расходники для рецептур селлеров (только с ценой и в наличии) getAvailableSuppliesForRecipe: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Селлеры могут получать расходники от своих фулфилмент-партнеров if (currentUser.organization.type !== 'SELLER') { return [] // Только селлеры используют рецептуры } // TODO: В будущем здесь будет логика получения расходников от партнерских фулфилментов // Пока возвращаем пустой массив, так как эта функциональность еще разрабатывается console.warn('🔥 getAvailableSuppliesForRecipe called for seller:', { sellerId: currentUser.organization.id, sellerName: currentUser.organization.name, }) return [] }, // Расходники фулфилмента из склада (новая архитектура - синхронизация со склада) myFulfillmentSupplies: async (_: unknown, __: unknown, context: Context) => { console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥') if (!context.user) { console.warn('❌ No user in context') throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) console.warn('👤 Current user:', { id: currentUser?.id, phone: currentUser?.phone, organizationId: currentUser?.organizationId, organizationType: currentUser?.organization?.type, organizationName: currentUser?.organization?.name, }) if (!currentUser?.organization) { console.warn('❌ No organization for user') throw new GraphQLError('У пользователя нет организации') } // Проверяем что это фулфилмент центр if (currentUser.organization.type !== 'FULFILLMENT') { console.warn('❌ User organization is not FULFILLMENT:', currentUser.organization.type) throw new GraphQLError('Доступ только для фулфилмент центров') } // Получаем расходники фулфилмента из таблицы Supply const supplies = await prisma.supply.findMany({ where: { organizationId: currentUser.organization.id, type: 'FULFILLMENT_CONSUMABLES', // Только расходники фулфилмента }, include: { organization: true, }, orderBy: { createdAt: 'desc' }, }) // Логирование для отладки console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥') console.warn('📊 Расходники фулфилмента из склада:', { organizationId: currentUser.organization.id, organizationType: currentUser.organization.type, suppliesCount: supplies.length, supplies: supplies.map((s) => ({ id: s.id, name: s.name, type: s.type, status: s.status, currentStock: s.currentStock, quantity: s.quantity, })), }) // Преобразуем в формат для фронтенда return supplies.map((supply) => ({ ...supply, price: supply.price ? parseFloat(supply.price.toString()) : 0, shippedQuantity: 0, // Добавляем для совместимости })) }, // Заказы поставок расходников supplyOrders: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } console.warn('🔍 SUPPLY ORDERS RESOLVER:', { userId: context.user.id, organizationType: currentUser.organization.type, organizationId: currentUser.organization.id, organizationName: currentUser.organization.name, }) try { // Возвращаем заказы где текущая организация является заказчиком, поставщиком, получателем или логистическим партнером const orders = await prisma.supplyOrder.findMany({ where: { OR: [ { organizationId: currentUser.organization.id }, // Заказы созданные организацией { partnerId: currentUser.organization.id }, // Заказы где организация - поставщик { fulfillmentCenterId: currentUser.organization.id }, // Заказы где организация - получатель (фулфилмент) { logisticsPartnerId: currentUser.organization.id }, // Заказы где организация - логистический партнер ], }, include: { partner: { include: { users: true, }, }, organization: { include: { users: true, }, }, fulfillmentCenter: { include: { users: true, }, }, logisticsPartner: true, items: { include: { product: { include: { category: true, organization: true, }, }, }, }, }, orderBy: { createdAt: 'desc' }, }) console.warn('📦 SUPPLY ORDERS FOUND:', { totalOrders: orders.length, ordersByRole: { asCreator: orders.filter((o) => o.organizationId === currentUser.organization.id).length, asPartner: orders.filter((o) => o.partnerId === currentUser.organization.id).length, asFulfillment: orders.filter((o) => o.fulfillmentCenterId === currentUser.organization.id).length, asLogistics: orders.filter((o) => o.logisticsPartnerId === currentUser.organization.id).length, }, orderStatuses: orders.reduce((acc: any, order) => { acc[order.status] = (acc[order.status] || 0) + 1 return acc }, {}), orderIds: orders.map((o) => o.id), }) return orders } catch (error) { console.error('❌ ERROR IN SUPPLY ORDERS RESOLVER:', error) throw new GraphQLError(`Ошибка получения заказов поставок: ${error}`) } }, // Счетчик поставок, требующих одобрения pendingSuppliesCount: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Считаем заказы поставок, требующие действий // Расходники фулфилмента (созданные нами для себя) - требуют действий по статусам const ourSupplyOrders = await prisma.supplyOrder.count({ where: { organizationId: currentUser.organization.id, // Создали мы fulfillmentCenterId: currentUser.organization.id, // Получатель - мы status: { in: ['CONFIRMED', 'IN_TRANSIT'] }, // Подтверждено или в пути }, }) // Расходники селлеров (созданные другими для нас) - требуют действий фулфилмента const sellerSupplyOrders = await prisma.supplyOrder.count({ where: { fulfillmentCenterId: currentUser.organization.id, // Получатель - мы organizationId: { not: currentUser.organization.id }, // Создали НЕ мы status: { in: [ 'SUPPLIER_APPROVED', // Поставщик подтвердил - нужно назначить логистику 'IN_TRANSIT', // В пути - нужно подтвердить получение ], }, }, }) // 🔔 ВХОДЯЩИЕ ЗАКАЗЫ ДЛЯ ПОСТАВЩИКОВ (WHOLESALE) - требуют подтверждения const incomingSupplierOrders = await prisma.supplyOrder.count({ where: { partnerId: currentUser.organization.id, // Мы - поставщик status: 'PENDING', // Ожидает подтверждения от поставщика }, }) // 🚚 ЛОГИСТИЧЕСКИЕ ЗАЯВКИ ДЛЯ ЛОГИСТИКИ (LOGIST) - требуют действий логистики const logisticsOrders = await prisma.supplyOrder.count({ where: { logisticsPartnerId: currentUser.organization.id, // Мы - назначенная логистика status: { in: [ 'CONFIRMED', // Подтверждено фулфилментом - нужно подтвердить логистикой 'LOGISTICS_CONFIRMED', // Подтверждено логистикой - нужно забрать товар у поставщика ], }, }, }) // Общий счетчик поставок в зависимости от типа организации let pendingSupplyOrders = 0 if (currentUser.organization.type === 'FULFILLMENT') { pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders } else if (currentUser.organization.type === 'WHOLESALE') { pendingSupplyOrders = incomingSupplierOrders } else if (currentUser.organization.type === 'LOGIST') { pendingSupplyOrders = logisticsOrders } else if (currentUser.organization.type === 'SELLER') { pendingSupplyOrders = 0 // Селлеры не подтверждают поставки, только отслеживают } // Считаем входящие заявки на партнерство со статусом PENDING const pendingIncomingRequests = await prisma.counterpartyRequest.count({ where: { receiverId: currentUser.organization.id, status: 'PENDING', }, }) return { supplyOrders: pendingSupplyOrders, ourSupplyOrders: ourSupplyOrders, // Расходники фулфилмента sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров incomingSupplierOrders: incomingSupplierOrders, // 🔔 Входящие заказы для поставщиков logisticsOrders: logisticsOrders, // 🚚 Логистические заявки для логистики incomingRequests: pendingIncomingRequests, total: pendingSupplyOrders + pendingIncomingRequests, } }, // Статистика склада фулфилмента с изменениями за сутки fulfillmentWarehouseStats: async (_: unknown, __: unknown, context: Context) => { console.warn('🔥 FULFILLMENT WAREHOUSE STATS RESOLVER CALLED') if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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 // Получаем дату начала суток (24 часа назад) const oneDayAgo = new Date() oneDayAgo.setDate(oneDayAgo.getDate() - 1) console.warn(`🏢 Organization ID: ${organizationId}, Date 24h ago: ${oneDayAgo.toISOString()}`) // Сначала проверим ВСЕ заказы поставок const allSupplyOrders = await prisma.supplyOrder.findMany({ where: { status: 'DELIVERED' }, include: { items: { include: { product: true }, }, organization: { select: { id: true, name: true, type: true } }, }, }) console.warn(`📦 ALL DELIVERED ORDERS: ${allSupplyOrders.length}`) allSupplyOrders.forEach((order) => { console.warn( ` Order ${order.id}: org=${order.organizationId} (${order.organization?.name}), fulfillment=${order.fulfillmentCenterId}, items=${order.items.length}`, ) }) // Продукты (товары от селлеров) - заказы К нам, но исключаем расходники фулфилмента const sellerDeliveredOrders = await prisma.supplyOrder.findMany({ where: { fulfillmentCenterId: organizationId, // Доставлено к нам (фулфилменту) organizationId: { not: organizationId }, // ИСПРАВЛЕНО: исключаем заказы самого фулфилмента status: 'DELIVERED', }, include: { items: { include: { product: true }, }, }, }) console.warn(`🛒 SELLER ORDERS TO FULFILLMENT: ${sellerDeliveredOrders.length}`) const productsCount = sellerDeliveredOrders.reduce( (sum, order) => sum + order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'PRODUCT' ? item.quantity : 0), 0), 0, ) // Изменения товаров за сутки (от селлеров) const recentSellerDeliveredOrders = await prisma.supplyOrder.findMany({ where: { fulfillmentCenterId: organizationId, // К нам organizationId: { not: organizationId }, // От селлеров status: 'DELIVERED', updatedAt: { gte: oneDayAgo }, }, include: { items: { include: { product: true }, }, }, }) const productsChangeToday = recentSellerDeliveredOrders.reduce( (sum, order) => sum + order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'PRODUCT' ? item.quantity : 0), 0), 0, ) // Товары (готовые товары = все продукты, не расходники) const goodsCount = productsCount // Готовые товары = все продукты const goodsChangeToday = productsChangeToday // Изменения товаров = изменения продуктов // Брак const defectsCount = 0 // TODO: реальные данные о браке const defectsChangeToday = 0 // Возвраты с ПВЗ const pvzReturnsCount = 0 // TODO: реальные данные о возвратах const pvzReturnsChangeToday = 0 // Расходники фулфилмента - заказы ОТ фулфилмента К поставщикам, НО доставленные на склад фулфилмента // Согласно правилам: фулфилмент заказывает расходники у поставщиков для своих операционных нужд const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({ where: { organizationId: organizationId, // Заказчик = фулфилмент fulfillmentCenterId: organizationId, // ИСПРАВЛЕНО: доставлено НА склад фулфилмента status: 'DELIVERED', }, include: { items: { include: { product: true }, }, }, }) console.warn(`🏭 FULFILLMENT SUPPLY ORDERS: ${fulfillmentSupplyOrders.length}`) // Подсчитываем количество из таблицы Supply (актуальные остатки на складе фулфилмента) // ИСПРАВЛЕНО: считаем только расходники фулфилмента, исключаем расходники селлеров const fulfillmentSuppliesFromWarehouse = await prisma.supply.findMany({ where: { organizationId: organizationId, // Склад фулфилмента type: 'FULFILLMENT_CONSUMABLES', // ТОЛЬКО расходники фулфилмента }, }) const fulfillmentSuppliesCount = fulfillmentSuppliesFromWarehouse.reduce( (sum, supply) => sum + (supply.currentStock || 0), 0, ) console.warn( `🔥 FULFILLMENT SUPPLIES DEBUG: organizationId=${organizationId}, ordersCount=${fulfillmentSupplyOrders.length}, warehouseCount=${fulfillmentSuppliesFromWarehouse.length}, totalStock=${fulfillmentSuppliesCount}`, ) console.warn( '📦 FULFILLMENT SUPPLIES BREAKDOWN:', fulfillmentSuppliesFromWarehouse.map((supply) => ({ name: supply.name, currentStock: supply.currentStock, supplier: supply.supplier, })), ) // Изменения расходников фулфилмента за сутки (ПРИБЫЛО) // Ищем заказы фулфилмента, доставленные на его склад за последние сутки const fulfillmentSuppliesReceivedToday = await prisma.supplyOrder.findMany({ where: { organizationId: organizationId, // Заказчик = фулфилмент fulfillmentCenterId: organizationId, // ИСПРАВЛЕНО: доставлено НА склад фулфилмента status: 'DELIVERED', updatedAt: { gte: oneDayAgo }, }, include: { items: { include: { product: true }, }, }, }) const fulfillmentSuppliesChangeToday = fulfillmentSuppliesReceivedToday.reduce( (sum, order) => sum + order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'CONSUMABLE' ? item.quantity : 0), 0), 0, ) console.warn( `📊 FULFILLMENT SUPPLIES RECEIVED TODAY (ПРИБЫЛО): ${fulfillmentSuppliesReceivedToday.length} orders, ${fulfillmentSuppliesChangeToday} items`, ) // Расходники селлеров - получаем из таблицы Supply (актуальные остатки на складе фулфилмента) // ИСПРАВЛЕНО: считаем из Supply с типом SELLER_CONSUMABLES const sellerSuppliesFromWarehouse = await prisma.supply.findMany({ where: { organizationId: organizationId, // Склад фулфилмента type: 'SELLER_CONSUMABLES', // ТОЛЬКО расходники селлеров }, }) const sellerSuppliesCount = sellerSuppliesFromWarehouse.reduce( (sum, supply) => sum + (supply.currentStock || 0), 0, ) console.warn(`💼 SELLER SUPPLIES DEBUG: totalCount=${sellerSuppliesCount} (from Supply warehouse)`) // Изменения расходников селлеров за сутки - считаем из Supply записей, созданных за сутки const sellerSuppliesReceivedToday = await prisma.supply.findMany({ where: { organizationId: organizationId, // Склад фулфилмента type: 'SELLER_CONSUMABLES', // ТОЛЬКО расходники селлеров createdAt: { gte: oneDayAgo }, // Созданы за последние сутки }, }) const sellerSuppliesChangeToday = sellerSuppliesReceivedToday.reduce( (sum, supply) => sum + (supply.currentStock || 0), 0, ) console.warn( `📊 SELLER SUPPLIES RECEIVED TODAY: ${sellerSuppliesReceivedToday.length} supplies, ${sellerSuppliesChangeToday} items`, ) // Вычисляем процентные изменения const calculatePercentChange = (current: number, change: number): number => { if (current === 0) return change > 0 ? 100 : 0 return (change / current) * 100 } const result = { products: { current: productsCount, change: productsChangeToday, percentChange: calculatePercentChange(productsCount, productsChangeToday), }, goods: { current: goodsCount, change: goodsChangeToday, percentChange: calculatePercentChange(goodsCount, goodsChangeToday), }, defects: { current: defectsCount, change: defectsChangeToday, percentChange: calculatePercentChange(defectsCount, defectsChangeToday), }, pvzReturns: { current: pvzReturnsCount, change: pvzReturnsChangeToday, percentChange: calculatePercentChange(pvzReturnsCount, pvzReturnsChangeToday), }, fulfillmentSupplies: { current: fulfillmentSuppliesCount, change: fulfillmentSuppliesChangeToday, percentChange: calculatePercentChange(fulfillmentSuppliesCount, fulfillmentSuppliesChangeToday), }, sellerSupplies: { current: sellerSuppliesCount, change: sellerSuppliesChangeToday, percentChange: calculatePercentChange(sellerSuppliesCount, sellerSuppliesChangeToday), }, } console.warn('🏁 FINAL WAREHOUSE STATS RESULT:', JSON.stringify(result, null, 2)) return result }, // Движения товаров (прибыло/убыло) за период supplyMovements: async (_: unknown, args: { period?: string }, context: Context) => { console.warn('🔄 SUPPLY MOVEMENTS RESOLVER CALLED with period:', args.period) if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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 console.warn(`🏢 SUPPLY MOVEMENTS for organization: ${organizationId}`) // Определяем период (по умолчанию 24 часа) const periodHours = args.period === '7d' ? 168 : args.period === '30d' ? 720 : 24 const periodAgo = new Date(Date.now() - periodHours * 60 * 60 * 1000) // ПРИБЫЛО: Поставки НА фулфилмент (статус DELIVERED за период) const arrivedOrders = await prisma.supplyOrder.findMany({ where: { fulfillmentCenterId: organizationId, status: 'DELIVERED', updatedAt: { gte: periodAgo }, }, include: { items: { include: { product: true }, }, }, }) console.warn(`📦 ARRIVED ORDERS: ${arrivedOrders.length}`) // Подсчитываем прибыло по типам const arrived = { products: 0, goods: 0, defects: 0, pvzReturns: 0, fulfillmentSupplies: 0, sellerSupplies: 0, } arrivedOrders.forEach((order) => { order.items.forEach((item) => { const quantity = item.quantity const productType = item.product?.type if (productType === 'PRODUCT') arrived.products += quantity else if (productType === 'GOODS') arrived.goods += quantity else if (productType === 'DEFECT') arrived.defects += quantity else if (productType === 'PVZ_RETURN') arrived.pvzReturns += quantity else if (productType === 'CONSUMABLE') { // Определяем тип расходника по заказчику if (order.organizationId === organizationId) { arrived.fulfillmentSupplies += quantity } else { arrived.sellerSupplies += quantity } } }) }) // УБЫЛО: Поставки НА маркетплейсы (по статусам отгрузки) // TODO: Пока возвращаем заглушки, нужно реализовать логику отгрузок const departed = { products: 0, // TODO: считать из отгрузок на WB/Ozon goods: 0, defects: 0, pvzReturns: 0, fulfillmentSupplies: 0, sellerSupplies: 0, } console.warn('📊 SUPPLY MOVEMENTS RESULT:', { arrived, departed }) return { arrived, departed, } }, // Логистика организации myLogistics: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } return await prisma.logistics.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true }, orderBy: { createdAt: 'desc' }, }) }, // Логистические партнеры (организации-логисты) logisticsPartners: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } // Получаем все организации типа LOGIST return await prisma.organization.findMany({ where: { type: 'LOGIST', // Убираем фильтр по статусу пока не определим правильные значения }, orderBy: { createdAt: 'desc' }, // Сортируем по дате создания вместо name }) }, // Мои поставки Wildberries myWildberriesSupplies: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } return await prisma.wildberriesSupply.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true, cards: true, }, orderBy: { createdAt: 'desc' }, }) }, // Расходники селлеров на складе фулфилмента (новый resolver) sellerSuppliesOnWarehouse: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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 sellerSupplies = await prisma.supply.findMany({ where: { organizationId: currentUser.organization.id, // На складе этого фулфилмента type: 'SELLER_CONSUMABLES' as const, // Только расходники селлеров sellerOwnerId: { not: null }, // ОБЯЗАТЕЛЬНО должен быть владелец-селлер }, include: { organization: true, // Фулфилмент-центр (хранитель) sellerOwner: true, // Селлер-владелец расходников }, orderBy: { createdAt: 'desc' }, }) // Логирование для отладки console.warn('🔍 ИСПРАВЛЕНО: Запрос расходников селлеров на складе фулфилмента:', { fulfillmentId: currentUser.organization.id, fulfillmentName: currentUser.organization.name, totalSupplies: sellerSupplies.length, sellerSupplies: sellerSupplies.map((supply) => ({ id: supply.id, name: supply.name, type: supply.type, sellerOwnerId: supply.sellerOwnerId, sellerOwnerName: supply.sellerOwner?.name || supply.sellerOwner?.fullName, currentStock: supply.currentStock, })), }) // ДВОЙНАЯ ПРОВЕРКА: Фильтруем на уровне кода для гарантии const filteredSupplies = sellerSupplies.filter((supply) => { const isValid = supply.type === 'SELLER_CONSUMABLES' && supply.sellerOwnerId != null && supply.sellerOwner != null if (!isValid) { console.warn('⚠️ ОТФИЛЬТРОВАН некорректный расходник:', { id: supply.id, name: supply.name, type: supply.type, sellerOwnerId: supply.sellerOwnerId, hasSellerOwner: !!supply.sellerOwner, }) } return isValid }) console.warn('✅ ФИНАЛЬНЫЙ РЕЗУЛЬТАТ после фильтрации:', { originalCount: sellerSupplies.length, filteredCount: filteredSupplies.length, removedCount: sellerSupplies.length - filteredSupplies.length, }) return filteredSupplies }, // Мои товары и расходники (для поставщиков) myProducts: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что это поставщик if (currentUser.organization.type !== 'WHOLESALE') { throw new GraphQLError('Товары доступны только для поставщиков') } const products = await prisma.product.findMany({ where: { organizationId: currentUser.organization.id, // Показываем и товары, и расходники поставщика }, include: { category: true, organization: true, }, orderBy: { createdAt: 'desc' }, }) console.warn('🔥 MY_PRODUCTS RESOLVER DEBUG:', { userId: currentUser.id, organizationId: currentUser.organization.id, organizationType: currentUser.organization.type, organizationName: currentUser.organization.name, totalProducts: products.length, productTypes: products.map((p) => ({ id: p.id, name: p.name, article: p.article, type: p.type, isActive: p.isActive, createdAt: p.createdAt, })), }) return products }, // Товары на складе фулфилмента (из доставленных заказов поставок) warehouseProducts: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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 deliveredSupplyOrders = await prisma.supplyOrder.findMany({ where: { fulfillmentCenterId: currentUser.organization.id, status: 'DELIVERED', // Только доставленные заказы }, include: { items: { include: { product: { include: { category: true, organization: true, // Включаем информацию о поставщике }, }, }, }, organization: true, // Селлер, который сделал заказ partner: true, // Поставщик товаров }, }) // Собираем все товары из доставленных заказов const allProducts: unknown[] = [] console.warn('🔍 Резолвер warehouseProducts (доставленные заказы):', { currentUserId: currentUser.id, organizationId: currentUser.organization.id, organizationType: currentUser.organization.type, deliveredOrdersCount: deliveredSupplyOrders.length, orders: deliveredSupplyOrders.map((order) => ({ id: order.id, sellerName: order.organization.name || order.organization.fullName, supplierName: order.partner.name || order.partner.fullName, status: order.status, itemsCount: order.items.length, deliveryDate: order.deliveryDate, })), }) for (const order of deliveredSupplyOrders) { console.warn( `📦 Заказ от селлера ${order.organization.name} у поставщика ${order.partner.name}:`, order.items.map((item) => ({ productId: item.product.id, productName: item.product.name, article: item.product.article, orderedQuantity: item.quantity, price: item.price, })), ) for (const item of order.items) { // Добавляем только товары типа PRODUCT, исключаем расходники if (item.product.type === 'PRODUCT') { allProducts.push({ ...item.product, // Дополнительная информация о заказе orderedQuantity: item.quantity, orderedPrice: item.price, orderId: order.id, orderDate: order.deliveryDate, seller: order.organization, // Селлер, который заказал supplier: order.partner, // Поставщик товара // Для совместимости с существующим интерфейсом organization: order.organization, // Указываем селлера как владельца }) } else { console.warn('🚫 Исключен расходник из основного склада фулфилмента:', { name: item.product.name, type: item.product.type, orderId: order.id, }) } } } console.warn('✅ Итого товаров на складе фулфилмента (из доставленных заказов):', allProducts.length) return allProducts }, // Данные склада с партнерами (3-уровневая иерархия) warehouseData: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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('Данные склада доступны только для фулфилмент центров') } console.warn('🏪 WAREHOUSE DATA: Получение данных склада для фулфилмента', currentUser.organization.id) // Получаем всех партнеров-селлеров const counterparties = await prisma.counterparty.findMany({ where: { organizationId: currentUser.organization.id, }, include: { counterparty: true, }, }) const sellerPartners = counterparties.filter((c) => c.counterparty.type === 'SELLER') console.warn('🤝 PARTNERS FOUND:', { totalCounterparties: counterparties.length, sellerPartners: sellerPartners.length, sellers: sellerPartners.map((p) => ({ id: p.counterparty.id, name: p.counterparty.name, fullName: p.counterparty.fullName, inn: p.counterparty.inn, })), }) // Создаем данные склада для каждого партнера-селлера const stores = sellerPartners.map((partner) => { const org = partner.counterparty // ЛОГИКА ОПРЕДЕЛЕНИЯ НАЗВАНИЯ МАГАЗИНА: // 1. Если есть name и оно не содержит "ИП" - используем name // 2. Если есть fullName и name содержит "ИП" - извлекаем из fullName название в скобках // 3. Fallback к name или fullName let storeName = org.name if (org.fullName && org.name?.includes('ИП')) { // Извлекаем название из скобок, например: "ИП Антипова Д. В. (Renrel)" -> "Renrel" const match = org.fullName.match(/\(([^)]+)\)/) if (match && match[1]) { storeName = match[1] } } return { id: `store_${org.id}`, storeName: storeName || org.fullName || org.name, storeOwner: org.inn || org.fullName || org.name, storeImage: org.logoUrl || null, storeQuantity: 0, // Пока без поставок partnershipDate: partner.createdAt || new Date(), products: [], // Пустой массив продуктов } }) // Сортировка: новые партнеры (quantity = 0) в самом верху stores.sort((a, b) => { if (a.storeQuantity === 0 && b.storeQuantity > 0) return -1 if (a.storeQuantity > 0 && b.storeQuantity === 0) return 1 return b.storeQuantity - a.storeQuantity }) console.warn('📦 WAREHOUSE STORES CREATED:', { storesCount: stores.length, storesPreview: stores.slice(0, 3).map((s) => ({ storeName: s.storeName, storeOwner: s.storeOwner, storeQuantity: s.storeQuantity, })), }) return { stores, } }, // Все товары и расходники поставщиков для маркета allProducts: async (_: unknown, args: { search?: string; category?: string }, context: Context) => { console.warn('🛍️ ALL_PRODUCTS RESOLVER - ВЫЗВАН:', { userId: context.user?.id, search: args.search, category: args.category, timestamp: new Date().toISOString(), }) if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const where: Record = { isActive: true, // Показываем только активные товары // Показываем и товары, и расходники поставщиков organization: { type: 'WHOLESALE', // Только товары поставщиков }, } if (args.search) { where.OR = [ { name: { contains: args.search, mode: 'insensitive' } }, { article: { contains: args.search, mode: 'insensitive' } }, { description: { contains: args.search, mode: 'insensitive' } }, { brand: { contains: args.search, mode: 'insensitive' } }, ] } if (args.category) { where.categoryId = args.category } const products = await prisma.product.findMany({ where, include: { category: true, organization: { include: { users: true, }, }, }, orderBy: { createdAt: 'desc' }, take: 100, // Ограничиваем количество результатов }) console.warn('🔥 ALL_PRODUCTS RESOLVER DEBUG:', { searchArgs: args, whereCondition: where, totalProducts: products.length, productTypes: products.map((p) => ({ id: p.id, name: p.name, type: p.type, org: p.organization.name, })), }) return products }, // Товары конкретной организации (для формы создания поставки) organizationProducts: async ( _: unknown, args: { organizationId: string; search?: string; category?: string; type?: string }, context: Context, ) => { console.warn('🏢 ORGANIZATION_PRODUCTS RESOLVER - ВЫЗВАН:', { userId: context.user?.id, organizationId: args.organizationId, search: args.search, category: args.category, type: args.type, timestamp: new Date().toISOString(), }) if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const where: Record = { isActive: true, // Показываем только активные товары organizationId: args.organizationId, // Фильтруем по конкретной организации type: args.type || 'ТОВАР', // Показываем только товары по умолчанию, НЕ расходники согласно development-checklist.md } if (args.search) { where.OR = [ { name: { contains: args.search, mode: 'insensitive' } }, { article: { contains: args.search, mode: 'insensitive' } }, { description: { contains: args.search, mode: 'insensitive' } }, { brand: { contains: args.search, mode: 'insensitive' } }, ] } if (args.category) { where.categoryId = args.category } const products = await prisma.product.findMany({ where, include: { category: true, organization: { include: { users: true, }, }, }, orderBy: { createdAt: 'desc' }, take: 100, // Ограничиваем количество результатов }) console.warn('🔥 ORGANIZATION_PRODUCTS RESOLVER DEBUG:', { organizationId: args.organizationId, searchArgs: args, whereCondition: where, totalProducts: products.length, productTypes: products.map((p) => ({ id: p.id, name: p.name, type: p.type, isActive: p.isActive, })), }) return products }, // Все категории categories: async (_: unknown, __: unknown, context: Context) => { if (!context.user && !context.admin) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } return await prisma.category.findMany({ orderBy: { name: 'asc' }, }) }, // Публичные услуги контрагента (для фулфилмента) counterpartyServices: async (_: unknown, args: { organizationId: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что запрашиваемая организация является контрагентом const counterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.organizationId, }, }) if (!counterparty) { throw new GraphQLError('Организация не является вашим контрагентом') } // Проверяем, что это фулфилмент центр const targetOrganization = await prisma.organization.findUnique({ where: { id: args.organizationId }, }) if (!targetOrganization || targetOrganization.type !== 'FULFILLMENT') { throw new GraphQLError('Услуги доступны только у фулфилмент центров') } return await prisma.service.findMany({ where: { organizationId: args.organizationId }, include: { organization: true }, orderBy: { createdAt: 'desc' }, }) }, // Публичные расходники контрагента (для поставщиков) counterpartySupplies: async (_: unknown, args: { organizationId: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что запрашиваемая организация является контрагентом const counterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.organizationId, }, }) if (!counterparty) { throw new GraphQLError('Организация не является вашим контрагентом') } // Проверяем, что это фулфилмент центр (у них есть расходники) const targetOrganization = await prisma.organization.findUnique({ where: { id: args.organizationId }, }) if (!targetOrganization || targetOrganization.type !== 'FULFILLMENT') { throw new GraphQLError('Расходники доступны только у фулфилмент центров') } return await prisma.supply.findMany({ where: { organizationId: args.organizationId }, include: { organization: true }, orderBy: { createdAt: 'desc' }, }) }, // Корзина пользователя myCart: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Найти или создать корзину для организации let cart = await prisma.cart.findUnique({ where: { organizationId: currentUser.organization.id }, include: { items: { include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, }, organization: true, }, }) if (!cart) { cart = await prisma.cart.create({ data: { organizationId: currentUser.organization.id, }, include: { items: { include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, }, organization: true, }, }) } return cart }, // Избранные товары пользователя myFavorites: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Получаем избранные товары const favorites = await prisma.favorites.findMany({ where: { organizationId: currentUser.organization.id }, include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, orderBy: { createdAt: 'desc' }, }) return favorites.map((favorite) => favorite.product) }, // Сотрудники организации myEmployees: async (_: unknown, __: unknown, context: Context) => { console.warn('🔍 myEmployees resolver called') if (!context.user) { console.warn('❌ No user in context for myEmployees') throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } console.warn('✅ User authenticated for myEmployees:', context.user.id) try { const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { console.warn('❌ User has no organization') throw new GraphQLError('У пользователя нет организации') } console.warn('📊 User organization type:', currentUser.organization.type) if (currentUser.organization.type !== 'FULFILLMENT') { console.warn('❌ Not a fulfillment center') throw new GraphQLError('Доступно только для фулфилмент центров') } const employees = await prisma.employee.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true, }, orderBy: { createdAt: 'desc' }, }) console.warn('👥 Found employees:', employees.length) return employees } catch (error) { console.error('❌ Error in myEmployees resolver:', error) throw error } }, // Получение сотрудника по ID employee: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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 employee = await prisma.employee.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, include: { organization: true, }, }) return employee }, // Получить табель сотрудника за месяц employeeSchedule: async ( _: unknown, args: { employeeId: string; year: number; month: number }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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 employee = await prisma.employee.findFirst({ where: { id: args.employeeId, organizationId: currentUser.organization.id, }, }) if (!employee) { throw new GraphQLError('Сотрудник не найден') } // Получаем записи табеля за указанный месяц const startDate = new Date(args.year, args.month, 1) const endDate = new Date(args.year, args.month + 1, 0) const scheduleRecords = await prisma.employeeSchedule.findMany({ where: { employeeId: args.employeeId, date: { gte: startDate, lte: endDate, }, }, orderBy: { date: 'asc', }, }) return scheduleRecords }, // Получить партнерскую ссылку текущего пользователя myPartnerLink: async (_: unknown, __: unknown, context: Context) => { if (!context.user?.organizationId) { throw new GraphQLError('Требуется авторизация и организация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const organization = await prisma.organization.findUnique({ where: { id: context.user.organizationId }, select: { referralCode: true }, }) if (!organization?.referralCode) { throw new GraphQLError('Реферальный код не найден') } return `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/register?partner=${organization.referralCode}` }, // Получить реферальную ссылку myReferralLink: async (_: unknown, __: unknown, context: Context) => { if (!context.user?.organizationId) { return 'http://localhost:3000/register?ref=PLEASE_LOGIN' } const organization = await prisma.organization.findUnique({ where: { id: context.user.organizationId }, select: { referralCode: true }, }) if (!organization?.referralCode) { throw new GraphQLError('Реферальный код не найден') } return `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/register?ref=${organization.referralCode}` }, // Статистика по рефералам myReferralStats: async (_: unknown, __: unknown, context: Context) => { if (!context.user?.organizationId) { throw new GraphQLError('Требуется авторизация и организация', { extensions: { code: 'UNAUTHENTICATED' }, }) } try { // Получаем текущие реферальные очки организации const organization = await prisma.organization.findUnique({ where: { id: context.user.organizationId }, select: { referralPoints: true }, }) // Получаем все транзакции где эта организация - реферер const transactions = await prisma.referralTransaction.findMany({ where: { referrerId: context.user.organizationId }, include: { referral: { select: { type: true, createdAt: true, }, }, }, }) // Подсчитываем статистику const totalSpheres = organization?.referralPoints || 0 const totalPartners = transactions.length // Партнеры за последний месяц const lastMonth = new Date() lastMonth.setMonth(lastMonth.getMonth() - 1) const monthlyPartners = transactions.filter((tx) => tx.createdAt > lastMonth).length const monthlySpheres = transactions .filter((tx) => tx.createdAt > lastMonth) .reduce((sum, tx) => sum + tx.points, 0) // Группировка по типам организаций const typeStats: Record = {} transactions.forEach((tx) => { const type = tx.referral.type if (!typeStats[type]) { typeStats[type] = { count: 0, spheres: 0 } } typeStats[type].count++ typeStats[type].spheres += tx.points }) // Группировка по источникам const sourceStats: Record = {} transactions.forEach((tx) => { const source = tx.type === 'REGISTRATION' ? 'REFERRAL_LINK' : 'AUTO_BUSINESS' if (!sourceStats[source]) { sourceStats[source] = { count: 0, spheres: 0 } } sourceStats[source].count++ sourceStats[source].spheres += tx.points }) return { totalPartners, totalSpheres, monthlyPartners, monthlySpheres, referralsByType: [ { type: 'SELLER', count: typeStats['SELLER']?.count || 0, spheres: typeStats['SELLER']?.spheres || 0 }, { type: 'WHOLESALE', count: typeStats['WHOLESALE']?.count || 0, spheres: typeStats['WHOLESALE']?.spheres || 0, }, { type: 'FULFILLMENT', count: typeStats['FULFILLMENT']?.count || 0, spheres: typeStats['FULFILLMENT']?.spheres || 0, }, { type: 'LOGIST', count: typeStats['LOGIST']?.count || 0, spheres: typeStats['LOGIST']?.spheres || 0 }, ], referralsBySource: [ { source: 'REFERRAL_LINK', count: sourceStats['REFERRAL_LINK']?.count || 0, spheres: sourceStats['REFERRAL_LINK']?.spheres || 0, }, { source: 'AUTO_BUSINESS', count: sourceStats['AUTO_BUSINESS']?.count || 0, spheres: sourceStats['AUTO_BUSINESS']?.spheres || 0, }, ], } } catch (error) { console.error('Ошибка получения статистики рефералов:', error) // Возвращаем заглушку в случае ошибки return { totalPartners: 0, totalSpheres: 0, monthlyPartners: 0, monthlySpheres: 0, referralsByType: [ { type: 'SELLER', count: 0, spheres: 0 }, { type: 'WHOLESALE', count: 0, spheres: 0 }, { type: 'FULFILLMENT', count: 0, spheres: 0 }, { type: 'LOGIST', count: 0, spheres: 0 }, ], referralsBySource: [ { source: 'REFERRAL_LINK', count: 0, spheres: 0 }, { source: 'AUTO_BUSINESS', count: 0, spheres: 0 }, ], } } }, // Получить список рефералов myReferrals: async (_: unknown, args: any, context: Context) => { if (!context.user?.organizationId) { throw new GraphQLError('Требуется авторизация и организация', { extensions: { code: 'UNAUTHENTICATED' }, }) } try { const { limit = 50, offset = 0 } = args || {} // Получаем рефералов (организации, которых пригласил текущий пользователь) const referralTransactions = await prisma.referralTransaction.findMany({ where: { referrerId: context.user.organizationId }, include: { referral: { select: { id: true, name: true, fullName: true, inn: true, type: true, createdAt: true, }, }, }, orderBy: { createdAt: 'desc' }, skip: offset, take: limit, }) // Преобразуем в формат для UI const referrals = referralTransactions.map((tx) => ({ id: tx.id, organization: tx.referral, source: tx.type === 'REGISTRATION' ? 'REFERRAL_LINK' : 'AUTO_BUSINESS', spheresEarned: tx.points, registeredAt: tx.createdAt.toISOString(), status: 'ACTIVE', })) // Получаем общее количество для пагинации const totalCount = await prisma.referralTransaction.count({ where: { referrerId: context.user.organizationId }, }) const totalPages = Math.ceil(totalCount / limit) return { referrals, totalCount, totalPages, } } catch (error) { console.error('Ошибка получения рефералов:', error) return { referrals: [], totalCount: 0, totalPages: 0, } } }, // Получить историю транзакций рефералов myReferralTransactions: async (_: unknown, args: { limit?: number; offset?: number }, context: Context) => { if (!context.user?.organizationId) { throw new GraphQLError('Требуется авторизация и организация', { extensions: { code: 'UNAUTHENTICATED' }, }) } try { // Временная заглушка для отладки const result = { transactions: [], totalCount: 0, } return result } catch (error) { console.error('Ошибка получения транзакций рефералов:', error) return { transactions: [], totalCount: 0, } } }, // 🔒 Мои поставки с системой безопасности (многоуровневая таблица) mySupplyOrders: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ const securityContext = createSecurityContext({ user: { id: currentUser.id, organizationId: currentUser.organization.id, organizationType: currentUser.organization.type, }, req: context.req, }) console.warn('🔍 GET MY SUPPLY ORDERS (SECURE):', { userId: context.user.id, organizationType: currentUser.organization.type, organizationId: currentUser.organization.id, securityEnabled: true, }) try { // 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ await ParticipantIsolation.validateAccess( prisma, currentUser.organization.id, currentUser.organization.type, 'SUPPLY_ORDER', ) // Определяем логику фильтрации в зависимости от типа организации let whereClause if (currentUser.organization.type === 'WHOLESALE') { // Поставщик видит заказы, где он является поставщиком (partnerId) whereClause = { partnerId: currentUser.organization.id, } } else { // Остальные (SELLER, FULFILLMENT) видят заказы, которые они создали (organizationId) whereClause = { organizationId: currentUser.organization.id, } } const supplyOrders = await prisma.supplyOrder.findMany({ where: whereClause, include: { partner: true, // Поставщик (уровень 3) organization: true, fulfillmentCenter: true, logisticsPartner: true, items: { // Товары (уровень 4) include: { product: { include: { category: true, organization: true, }, }, }, orderBy: { createdAt: 'asc', }, }, }, orderBy: { createdAt: 'desc', // Новые поставки сверху (по номеру) }, }) console.warn('📦 Найдено поставок (до фильтрации):', supplyOrders.length, { organizationType: currentUser.organization.type, filterType: currentUser.organization.type === 'WHOLESALE' ? 'partnerId' : 'organizationId', organizationId: currentUser.organization.id, }) // 🔒 ПРИМЕНЕНИЕ СИСТЕМЫ БЕЗОПАСНОСТИ К КАЖДОМУ ЗАКАЗУ const secureProcessedOrders = await Promise.all( supplyOrders.map(async (order) => { // 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ await CommercialDataAudit.logAccess(prisma, { userId: currentUser.id, organizationType: currentUser.organization.type, action: 'VIEW_PRICE', resourceType: 'SUPPLY_ORDER', resourceId: order.id, metadata: { orderStatus: order.status, totalAmount: order.totalAmount, partner: order.partner?.name || order.partner?.inn, }, ipAddress: securityContext.ipAddress, userAgent: securityContext.userAgent, }) // 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ПО РОЛИ const filteredOrder = SupplyDataFilter.filterSupplyOrder(order, securityContext) // Обрабатываем каждый товар для получения рецептуры с фильтрацией const processedItems = await Promise.all( filteredOrder.data.items.map(async (item: any) => { let recipe = null // Получаем развернутую рецептуру если есть данные if ( item.services?.length > 0 || item.fulfillmentConsumables?.length > 0 || item.sellerConsumables?.length > 0 ) { // 🔒 АУДИТ ДОСТУПА К РЕЦЕПТУРЕ await CommercialDataAudit.logAccess(prisma, { userId: currentUser.id, organizationType: currentUser.organization.type, action: 'VIEW_RECIPE', resourceType: 'SUPPLY_ORDER', resourceId: item.id, metadata: { hasServices: item.services?.length > 0, hasFulfillmentConsumables: item.fulfillmentConsumables?.length > 0, hasSellerConsumables: item.sellerConsumables?.length > 0, }, ipAddress: securityContext.ipAddress, userAgent: securityContext.userAgent, }) // Получаем услуги с фильтрацией const services = item.services?.length > 0 ? await prisma.service.findMany({ where: { id: { in: item.services } }, include: { organization: true }, }) : [] // Получаем расходники фулфилмента с фильтрацией const fulfillmentConsumables = item.fulfillmentConsumables?.length > 0 ? await prisma.supply.findMany({ where: { id: { in: item.fulfillmentConsumables } }, include: { organization: true }, }) : [] // Получаем расходники селлера с фильтрацией const sellerConsumables = item.sellerConsumables?.length > 0 ? await prisma.supply.findMany({ where: { id: { in: item.sellerConsumables } }, }) : [] // 🔒 ФИЛЬТРАЦИЯ РЕЦЕПТУРЫ ПО РОЛИ // Для WHOLESALE скрываем рецептуру полностью if (currentUser.organization.type === 'WHOLESALE') { recipe = null } else { recipe = { services, fulfillmentConsumables, sellerConsumables, marketplaceCardId: item.marketplaceCardId, } } } return { ...item, price: item.price || 0, // Исправлено: защита от null значения в существующих данных recipe, } }), ) return { ...filteredOrder.data, items: processedItems, // 🔒 ДОБАВЛЯЕМ МЕТАДАННЫЕ БЕЗОПАСНОСТИ _security: { filtered: filteredOrder.filtered, removedFields: filteredOrder.removedFields, accessLevel: filteredOrder.accessLevel, }, } }), ) console.warn('✅ Данные обработаны с системой безопасности:', { ordersTotal: secureProcessedOrders.length, securityApplied: true, organizationType: currentUser.organization.type, }) return secureProcessedOrders } catch (error) { console.error('❌ Ошибка получения поставок (security):', error) throw new GraphQLError(`Ошибка получения поставок: ${error instanceof Error ? error.message : String(error)}`) } }, // Новая система поставок v2 ...fulfillmentConsumableV2Queries, }, Mutation: { sendSmsCode: async (_: unknown, args: { phone: string }) => { const result = await smsService.sendSmsCode(args.phone) return { success: result.success, message: result.message || 'SMS код отправлен', } }, verifySmsCode: async (_: unknown, args: { phone: string; code: string }) => { const verificationResult = await smsService.verifySmsCode(args.phone, args.code) if (!verificationResult.success) { return { success: false, message: verificationResult.message || 'Неверный код', } } // Найти или создать пользователя const formattedPhone = args.phone.replace(/\D/g, '') let user = await prisma.user.findUnique({ where: { phone: formattedPhone }, include: { organization: { include: { apiKeys: true, }, }, }, }) if (!user) { user = await prisma.user.create({ data: { phone: formattedPhone, }, include: { organization: { include: { apiKeys: true, }, }, }, }) } const token = generateToken({ userId: user.id, phone: user.phone, }) console.warn('verifySmsCode - Generated token:', token ? `${token.substring(0, 20)}...` : 'No token') console.warn('verifySmsCode - Full token:', token) console.warn('verifySmsCode - User object:', { id: user.id, phone: user.phone, }) const result = { success: true, message: 'Авторизация успешна', token, user, } console.warn('verifySmsCode - Returning result:', { success: result.success, hasToken: !!result.token, hasUser: !!result.user, message: result.message, tokenPreview: result.token ? `${result.token.substring(0, 20)}...` : 'No token in result', }) return result }, verifyInn: async (_: unknown, args: { inn: string }) => { // Валидируем ИНН if (!dadataService.validateInn(args.inn)) { return { success: false, message: 'Неверный формат ИНН', } } // Получаем данные организации из DaData const organizationData = await dadataService.getOrganizationByInn(args.inn) if (!organizationData) { return { success: false, message: 'Организация с указанным ИНН не найдена', } } return { success: true, message: 'ИНН найден', organization: { name: organizationData.name, fullName: organizationData.fullName, address: organizationData.address, isActive: organizationData.isActive, }, } }, registerFulfillmentOrganization: async ( _: unknown, args: { input: { phone: string inn: string type: 'FULFILLMENT' | 'LOGIST' | 'WHOLESALE' referralCode?: string partnerCode?: string } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const { inn, type, referralCode, partnerCode } = args.input // Валидируем ИНН if (!dadataService.validateInn(inn)) { return { success: false, message: 'Неверный формат ИНН', } } // Получаем данные организации из DaData const organizationData = await dadataService.getOrganizationByInn(inn) if (!organizationData) { return { success: false, message: 'Организация с указанным ИНН не найдена', } } try { // Проверяем, что организация еще не зарегистрирована const existingOrg = await prisma.organization.findUnique({ where: { inn: organizationData.inn }, }) if (existingOrg) { return { success: false, message: 'Организация с таким ИНН уже зарегистрирована', } } // Генерируем уникальный реферальный код const generatedReferralCode = await generateReferralCode() // Создаем организацию со всеми данными из DaData const organization = await prisma.organization.create({ data: { inn: organizationData.inn, kpp: organizationData.kpp, name: organizationData.name, fullName: organizationData.fullName, address: organizationData.address, addressFull: organizationData.addressFull, ogrn: organizationData.ogrn, ogrnDate: organizationData.ogrnDate, // Статус организации status: organizationData.status, actualityDate: organizationData.actualityDate, registrationDate: organizationData.registrationDate, liquidationDate: organizationData.liquidationDate, // Руководитель managementName: organizationData.managementName, managementPost: organizationData.managementPost, // ОПФ opfCode: organizationData.opfCode, opfFull: organizationData.opfFull, opfShort: organizationData.opfShort, // Коды статистики okato: organizationData.okato, oktmo: organizationData.oktmo, okpo: organizationData.okpo, okved: organizationData.okved, // Контакты phones: organizationData.phones ? JSON.parse(JSON.stringify(organizationData.phones)) : null, emails: organizationData.emails ? JSON.parse(JSON.stringify(organizationData.emails)) : null, // Финансовые данные employeeCount: organizationData.employeeCount, revenue: organizationData.revenue, taxSystem: organizationData.taxSystem, type: type, dadataData: JSON.parse(JSON.stringify(organizationData.rawData)), // Реферальная система - генерируем код автоматически referralCode: generatedReferralCode, }, }) // Привязываем пользователя к организации const updatedUser = await prisma.user.update({ where: { id: context.user.id }, data: { organizationId: organization.id }, include: { organization: { include: { apiKeys: true, }, }, }, }) // Обрабатываем реферальные коды if (referralCode) { try { // Находим реферера по реферальному коду const referrer = await prisma.organization.findUnique({ where: { referralCode: referralCode }, }) if (referrer) { // Создаем реферальную транзакцию (100 сфер) await prisma.referralTransaction.create({ data: { referrerId: referrer.id, referralId: organization.id, points: 100, type: 'REGISTRATION', description: `Регистрация ${type.toLowerCase()} организации по реферальной ссылке`, }, }) // Увеличиваем счетчик сфер у реферера await prisma.organization.update({ where: { id: referrer.id }, data: { referralPoints: { increment: 100 } }, }) // Устанавливаем связь реферала и источник регистрации await prisma.organization.update({ where: { id: organization.id }, data: { referredById: referrer.id }, }) } } catch { // Error processing referral code, but continue registration } } if (partnerCode) { try { // Находим партнера по партнерскому коду const partner = await prisma.organization.findUnique({ where: { referralCode: partnerCode }, }) if (partner) { // Создаем реферальную транзакцию (100 сфер) await prisma.referralTransaction.create({ data: { referrerId: partner.id, referralId: organization.id, points: 100, type: 'AUTO_PARTNERSHIP', description: `Регистрация ${type.toLowerCase()} организации по партнерской ссылке`, }, }) // Увеличиваем счетчик сфер у партнера await prisma.organization.update({ where: { id: partner.id }, data: { referralPoints: { increment: 100 } }, }) // Устанавливаем связь реферала и источник регистрации await prisma.organization.update({ where: { id: organization.id }, data: { referredById: partner.id }, }) // Создаем партнерскую связь (автоматическое добавление в контрагенты) await prisma.counterparty.create({ data: { organizationId: partner.id, counterpartyId: organization.id, type: 'AUTO', triggeredBy: 'PARTNER_LINK', }, }) await prisma.counterparty.create({ data: { organizationId: organization.id, counterpartyId: partner.id, type: 'AUTO', triggeredBy: 'PARTNER_LINK', }, }) } } catch { // Error processing partner code, but continue registration } } return { success: true, message: 'Организация успешно зарегистрирована', user: updatedUser, } } catch { // Error registering fulfillment organization return { success: false, message: 'Ошибка при регистрации организации', } } }, registerSellerOrganization: async ( _: unknown, args: { input: { phone: string wbApiKey?: string ozonApiKey?: string ozonClientId?: string referralCode?: string partnerCode?: string } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const { wbApiKey, ozonApiKey, ozonClientId, referralCode, partnerCode } = args.input if (!wbApiKey && !ozonApiKey) { return { success: false, message: 'Необходимо указать хотя бы один API ключ маркетплейса', } } try { // Валидируем API ключи const validationResults = [] if (wbApiKey) { const wbResult = await marketplaceService.validateWildberriesApiKey(wbApiKey) if (!wbResult.isValid) { return { success: false, message: `Wildberries: ${wbResult.message}`, } } validationResults.push({ marketplace: 'WILDBERRIES', apiKey: wbApiKey, data: wbResult.data, }) } if (ozonApiKey && ozonClientId) { const ozonResult = await marketplaceService.validateOzonApiKey(ozonApiKey, ozonClientId) if (!ozonResult.isValid) { return { success: false, message: `Ozon: ${ozonResult.message}`, } } validationResults.push({ marketplace: 'OZON', apiKey: ozonApiKey, data: ozonResult.data, }) } // Создаем организацию селлера - используем tradeMark как основное имя const tradeMark = validationResults[0]?.data?.tradeMark const sellerName = validationResults[0]?.data?.sellerName const shopName = tradeMark || sellerName || 'Магазин' // Генерируем уникальный реферальный код const generatedReferralCode = await generateReferralCode() const organization = await prisma.organization.create({ data: { inn: (validationResults[0]?.data?.inn as string) || `SELLER_${Date.now()}`, name: shopName, // Используем tradeMark как основное название fullName: sellerName ? `${sellerName} (${shopName})` : `Интернет-магазин "${shopName}"`, type: 'SELLER', // Реферальная система - генерируем код автоматически referralCode: generatedReferralCode, }, }) // Добавляем API ключи for (const validation of validationResults) { await prisma.apiKey.create({ data: { marketplace: validation.marketplace as 'WILDBERRIES' | 'OZON', apiKey: validation.apiKey, organizationId: organization.id, validationData: JSON.parse(JSON.stringify(validation.data)), }, }) } // Привязываем пользователя к организации const updatedUser = await prisma.user.update({ where: { id: context.user.id }, data: { organizationId: organization.id }, include: { organization: { include: { apiKeys: true, }, }, }, }) // Обрабатываем реферальные коды if (referralCode) { try { // Находим реферера по реферальному коду const referrer = await prisma.organization.findUnique({ where: { referralCode: referralCode }, }) if (referrer) { // Создаем реферальную транзакцию (100 сфер) await prisma.referralTransaction.create({ data: { referrerId: referrer.id, referralId: organization.id, points: 100, type: 'REGISTRATION', description: 'Регистрация селлер организации по реферальной ссылке', }, }) // Увеличиваем счетчик сфер у реферера await prisma.organization.update({ where: { id: referrer.id }, data: { referralPoints: { increment: 100 } }, }) // Устанавливаем связь реферала и источник регистрации await prisma.organization.update({ where: { id: organization.id }, data: { referredById: referrer.id }, }) } } catch { // Error processing referral code, but continue registration } } if (partnerCode) { try { // Находим партнера по партнерскому коду const partner = await prisma.organization.findUnique({ where: { referralCode: partnerCode }, }) if (partner) { // Создаем реферальную транзакцию (100 сфер) await prisma.referralTransaction.create({ data: { referrerId: partner.id, referralId: organization.id, points: 100, type: 'AUTO_PARTNERSHIP', description: 'Регистрация селлер организации по партнерской ссылке', }, }) // Увеличиваем счетчик сфер у партнера await prisma.organization.update({ where: { id: partner.id }, data: { referralPoints: { increment: 100 } }, }) // Устанавливаем связь реферала и источник регистрации await prisma.organization.update({ where: { id: organization.id }, data: { referredById: partner.id }, }) // Создаем партнерскую связь (автоматическое добавление в контрагенты) await prisma.counterparty.create({ data: { organizationId: partner.id, counterpartyId: organization.id, type: 'AUTO', triggeredBy: 'PARTNER_LINK', }, }) await prisma.counterparty.create({ data: { organizationId: organization.id, counterpartyId: partner.id, type: 'AUTO', triggeredBy: 'PARTNER_LINK', }, }) } } catch { // Error processing partner code, but continue registration } } return { success: true, message: 'Селлер организация успешно зарегистрирована', user: updatedUser, } } catch { // Error registering seller organization return { success: false, message: 'Ошибка при регистрации организации', } } }, addMarketplaceApiKey: async ( _: unknown, args: { input: { marketplace: 'WILDBERRIES' | 'OZON' apiKey: string clientId?: string validateOnly?: boolean } }, context: Context, ) => { // Разрешаем валидацию без авторизации if (!args.input.validateOnly && !context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const { marketplace, apiKey, clientId, validateOnly } = args.input console.warn(`🔍 Validating ${marketplace} API key:`, { keyLength: apiKey.length, keyPreview: apiKey.substring(0, 20) + '...', validateOnly, }) // Валидируем API ключ const validationResult = await marketplaceService.validateApiKey(marketplace, apiKey, clientId) console.warn(`✅ Validation result for ${marketplace}:`, validationResult) if (!validationResult.isValid) { console.warn(`❌ Validation failed for ${marketplace}:`, validationResult.message) return { success: false, message: validationResult.message, } } // Если это только валидация, возвращаем результат без сохранения if (validateOnly) { return { success: true, message: 'API ключ действителен', apiKey: { id: 'validate-only', marketplace, apiKey: '***', // Скрываем реальный ключ при валидации isActive: true, validationData: validationResult, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, } } // Для сохранения API ключа нужна авторизация if (!context.user) { throw new GraphQLError('Требуется авторизация для сохранения API ключа', { extensions: { code: 'UNAUTHENTICATED' }, }) } const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!user?.organization) { return { success: false, message: 'Пользователь не привязан к организации', } } try { // Проверяем, что такого ключа еще нет const existingKey = await prisma.apiKey.findUnique({ where: { organizationId_marketplace: { organizationId: user.organization.id, marketplace, }, }, }) if (existingKey) { // Обновляем существующий ключ const updatedKey = await prisma.apiKey.update({ where: { id: existingKey.id }, data: { apiKey, validationData: JSON.parse(JSON.stringify(validationResult.data)), isActive: true, }, }) return { success: true, message: 'API ключ успешно обновлен', apiKey: updatedKey, } } else { // Создаем новый ключ const newKey = await prisma.apiKey.create({ data: { marketplace, apiKey, organizationId: user.organization.id, validationData: JSON.parse(JSON.stringify(validationResult.data)), }, }) return { success: true, message: 'API ключ успешно добавлен', apiKey: newKey, } } } catch (error) { console.error('Error adding marketplace API key:', error) return { success: false, message: 'Ошибка при добавлении API ключа', } } }, removeMarketplaceApiKey: async (_: unknown, args: { marketplace: 'WILDBERRIES' | 'OZON' }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!user?.organization) { throw new GraphQLError('Пользователь не привязан к организации') } try { await prisma.apiKey.delete({ where: { organizationId_marketplace: { organizationId: user.organization.id, marketplace: args.marketplace, }, }, }) return true } catch (error) { console.error('Error removing marketplace API key:', error) return false } }, updateUserProfile: async ( _: unknown, args: { input: { avatar?: string orgPhone?: string managerName?: string telegram?: string whatsapp?: string email?: string bankName?: string bik?: string accountNumber?: string corrAccount?: string market?: string } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }) if (!user?.organization) { throw new GraphQLError('Пользователь не привязан к организации') } try { const { input } = args // Обновляем данные пользователя (аватар, имя управляющего) const userUpdateData: { avatar?: string; managerName?: string } = {} if (input.avatar) { userUpdateData.avatar = input.avatar } if (input.managerName) { userUpdateData.managerName = input.managerName } if (Object.keys(userUpdateData).length > 0) { await prisma.user.update({ where: { id: context.user.id }, data: userUpdateData, }) } // Подготавливаем данные для обновления организации const updateData: { phones?: object emails?: object managementName?: string managementPost?: string market?: string } = {} // Название организации больше не обновляется через профиль // Для селлеров устанавливается при регистрации, для остальных - при смене ИНН // Обновляем контактные данные в JSON поле phones if (input.orgPhone) { updateData.phones = [{ value: input.orgPhone, type: 'main' }] } // Обновляем email в JSON поле emails if (input.email) { updateData.emails = [{ value: input.email, type: 'main' }] } // Обновляем рынок для поставщиков if (input.market !== undefined) { updateData.market = input.market === 'none' ? null : input.market } // Сохраняем дополнительные контакты в custom полях // Пока добавим их как дополнительные JSON поля const customContacts: { managerName?: string telegram?: string whatsapp?: string bankDetails?: { bankName?: string bik?: string accountNumber?: string corrAccount?: string } } = {} // managerName теперь сохраняется в поле пользователя, а не в JSON if (input.telegram) { customContacts.telegram = input.telegram } if (input.whatsapp) { customContacts.whatsapp = input.whatsapp } if (input.bankName || input.bik || input.accountNumber || input.corrAccount) { customContacts.bankDetails = { bankName: input.bankName, bik: input.bik, accountNumber: input.accountNumber, corrAccount: input.corrAccount, } } // Если есть дополнительные контакты, сохраним их в поле managementPost временно // В идеале нужно добавить отдельную таблицу для контактов if (Object.keys(customContacts).length > 0) { updateData.managementPost = JSON.stringify(customContacts) } // Обновляем организацию await prisma.organization.update({ where: { id: user.organization.id }, data: updateData, include: { apiKeys: true, }, }) // Получаем обновленного пользователя const updatedUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }) return { success: true, message: 'Профиль успешно обновлен', user: updatedUser, } } catch (error) { console.error('Error updating user profile:', error) return { success: false, message: 'Ошибка при обновлении профиля', } } }, updateOrganizationByInn: async (_: unknown, args: { inn: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }) if (!user?.organization) { throw new GraphQLError('Пользователь не привязан к организации') } try { // Валидируем ИНН if (!dadataService.validateInn(args.inn)) { return { success: false, message: 'Неверный формат ИНН', } } // Получаем данные организации из DaData const organizationData = await dadataService.getOrganizationByInn(args.inn) if (!organizationData) { return { success: false, message: 'Организация с указанным ИНН не найдена в федеральном реестре', } } // Проверяем, есть ли уже организация с таким ИНН в базе (кроме текущей) const existingOrganization = await prisma.organization.findUnique({ where: { inn: organizationData.inn }, }) if (existingOrganization && existingOrganization.id !== user.organization.id) { return { success: false, message: `Организация с ИНН ${organizationData.inn} уже существует в системе`, } } // Подготавливаем данные для обновления const updateData: Prisma.OrganizationUpdateInput = { kpp: organizationData.kpp, // Для селлеров не обновляем название организации (это название магазина) ...(user.organization.type !== 'SELLER' && { name: organizationData.name, }), fullName: organizationData.fullName, address: organizationData.address, addressFull: organizationData.addressFull, ogrn: organizationData.ogrn, ogrnDate: organizationData.ogrnDate ? organizationData.ogrnDate.toISOString() : null, registrationDate: organizationData.registrationDate ? organizationData.registrationDate.toISOString() : null, liquidationDate: organizationData.liquidationDate ? organizationData.liquidationDate.toISOString() : null, managementName: organizationData.managementName, // Всегда перезаписываем данными из DaData (может быть null) managementPost: user.organization.managementPost, // Сохраняем кастомные данные пользователя opfCode: organizationData.opfCode, opfFull: organizationData.opfFull, opfShort: organizationData.opfShort, okato: organizationData.okato, oktmo: organizationData.oktmo, okpo: organizationData.okpo, okved: organizationData.okved, status: organizationData.status, } // Добавляем ИНН только если он отличается от текущего if (user.organization.inn !== organizationData.inn) { updateData.inn = organizationData.inn } // Обновляем организацию await prisma.organization.update({ where: { id: user.organization.id }, data: updateData, include: { apiKeys: true, }, }) // Получаем обновленного пользователя const updatedUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }) return { success: true, message: 'Данные организации успешно обновлены', user: updatedUser, } } catch (error) { console.error('Error updating organization by INN:', error) return { success: false, message: 'Ошибка при обновлении данных организации', } } }, logout: () => { // В stateless JWT системе logout происходит на клиенте // Можно добавить blacklist токенов, если нужно return true }, // Отправить заявку на добавление в контрагенты sendCounterpartyRequest: async ( _: unknown, args: { organizationId: string; message?: string }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } if (currentUser.organization.id === args.organizationId) { throw new GraphQLError('Нельзя отправить заявку самому себе') } // Проверяем, что организация-получатель существует const receiverOrganization = await prisma.organization.findUnique({ where: { id: args.organizationId }, }) if (!receiverOrganization) { throw new GraphQLError('Организация не найдена') } try { // Создаем или обновляем заявку const request = await prisma.counterpartyRequest.upsert({ where: { senderId_receiverId: { senderId: currentUser.organization.id, receiverId: args.organizationId, }, }, update: { status: 'PENDING', message: args.message, updatedAt: new Date(), }, create: { senderId: currentUser.organization.id, receiverId: args.organizationId, message: args.message, status: 'PENDING', }, include: { sender: true, receiver: true, }, }) // Уведомляем получателя о новой заявке try { notifyOrganization(args.organizationId, { type: 'counterparty:request:new', payload: { requestId: request.id, senderId: request.senderId, receiverId: request.receiverId, }, }) } catch {} return { success: true, message: 'Заявка отправлена', request, } } catch (error) { console.error('Error sending counterparty request:', error) return { success: false, message: 'Ошибка при отправке заявки', } } }, // Ответить на заявку контрагента respondToCounterpartyRequest: async ( _: unknown, args: { requestId: string; accept: boolean }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { // Найти заявку и проверить права const request = await prisma.counterpartyRequest.findUnique({ where: { id: args.requestId }, include: { sender: true, receiver: true, }, }) if (!request) { throw new GraphQLError('Заявка не найдена') } if (request.receiverId !== currentUser.organization.id) { throw new GraphQLError('Нет прав на обработку этой заявки') } if (request.status !== 'PENDING') { throw new GraphQLError('Заявка уже обработана') } const newStatus = args.accept ? 'ACCEPTED' : 'REJECTED' // Обновляем статус заявки const updatedRequest = await prisma.counterpartyRequest.update({ where: { id: args.requestId }, data: { status: newStatus }, include: { sender: true, receiver: true, }, }) // Если заявка принята, создаем связи контрагентов в обе стороны if (args.accept) { await prisma.$transaction([ // Добавляем отправителя в контрагенты получателя prisma.counterparty.create({ data: { organizationId: request.receiverId, counterpartyId: request.senderId, }, }), // Добавляем получателя в контрагенты отправителя prisma.counterparty.create({ data: { organizationId: request.senderId, counterpartyId: request.receiverId, }, }), ]) // АВТОМАТИЧЕСКОЕ СОЗДАНИЕ ЗАПИСЕЙ В ТАБЛИЦЕ СКЛАДА ФУЛФИЛМЕНТА // Проверяем, есть ли фулфилмент среди партнеров if (request.receiver.type === 'FULFILLMENT' && request.sender.type === 'SELLER') { // Селлер становится партнером фулфилмента - создаем запись склада try { await autoCreateWarehouseEntry(request.senderId, request.receiverId) console.warn( `✅ AUTO WAREHOUSE ENTRY: Created for seller ${request.senderId} with fulfillment ${request.receiverId}`, ) } catch (error) { console.error('❌ AUTO WAREHOUSE ENTRY ERROR:', error) // Не прерываем основной процесс, если не удалось создать запись склада } } else if (request.sender.type === 'FULFILLMENT' && request.receiver.type === 'SELLER') { // Фулфилмент принимает заявку от селлера - создаем запись склада try { await autoCreateWarehouseEntry(request.receiverId, request.senderId) console.warn( `✅ AUTO WAREHOUSE ENTRY: Created for seller ${request.receiverId} with fulfillment ${request.senderId}`, ) } catch (error) { console.error('❌ AUTO WAREHOUSE ENTRY ERROR:', error) } } } // Оповещаем обе стороны об обновлении заявки и возможном изменении списка контрагентов try { notifyMany([request.senderId, request.receiverId], { type: 'counterparty:request:updated', payload: { requestId: updatedRequest.id, status: updatedRequest.status }, }) } catch {} return { success: true, message: args.accept ? 'Заявка принята' : 'Заявка отклонена', request: updatedRequest, } } catch (error) { console.error('Error responding to counterparty request:', error) return { success: false, message: 'Ошибка при обработке заявки', } } }, // Отменить заявку cancelCounterpartyRequest: async (_: unknown, args: { requestId: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { const request = await prisma.counterpartyRequest.findUnique({ where: { id: args.requestId }, }) if (!request) { throw new GraphQLError('Заявка не найдена') } if (request.senderId !== currentUser.organization.id) { throw new GraphQLError('Можно отменить только свои заявки') } if (request.status !== 'PENDING') { throw new GraphQLError('Можно отменить только ожидающие заявки') } await prisma.counterpartyRequest.update({ where: { id: args.requestId }, data: { status: 'CANCELLED' }, }) return true } catch (error) { console.error('Error cancelling counterparty request:', error) return false } }, // Удалить контрагента removeCounterparty: async (_: unknown, args: { organizationId: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { // Удаляем связь в обе стороны await prisma.$transaction([ prisma.counterparty.deleteMany({ where: { organizationId: currentUser.organization.id, counterpartyId: args.organizationId, }, }), prisma.counterparty.deleteMany({ where: { organizationId: args.organizationId, counterpartyId: currentUser.organization.id, }, }), ]) return true } catch (error) { console.error('Error removing counterparty:', error) return false } }, // Автоматическое создание записи в таблице склада autoCreateWarehouseEntry: async (_: unknown, args: { partnerId: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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('Только фулфилмент может создавать записи склада') } try { // Получаем данные партнера-селлера const partnerOrg = await prisma.organization.findUnique({ where: { id: args.partnerId }, }) if (!partnerOrg) { throw new GraphQLError('Партнер не найден') } if (partnerOrg.type !== 'SELLER') { throw new GraphQLError('Автозаписи создаются только для партнеров-селлеров') } // Создаем запись склада const warehouseEntry = await autoCreateWarehouseEntry(args.partnerId, currentUser.organization.id) return { success: true, message: 'Запись склада создана успешно', warehouseEntry, } } catch (error) { console.error('Error creating auto warehouse entry:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка создания записи склада', } } }, // Отправить сообщение sendMessage: async ( _: unknown, args: { receiverOrganizationId: string content?: string type?: 'TEXT' | 'VOICE' }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что получатель является контрагентом const isCounterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.receiverOrganizationId, }, }) if (!isCounterparty) { throw new GraphQLError('Можно отправлять сообщения только контрагентам') } // Получаем организацию получателя const receiverOrganization = await prisma.organization.findUnique({ where: { id: args.receiverOrganizationId }, }) if (!receiverOrganization) { throw new GraphQLError('Организация получателя не найдена') } try { // Создаем сообщение const message = await prisma.message.create({ data: { content: args.content?.trim() || null, type: args.type || 'TEXT', senderId: context.user.id, senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.receiverOrganizationId, }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, }) // Реалтайм нотификация для обеих организаций (отправитель и получатель) try { notifyMany([currentUser.organization.id, args.receiverOrganizationId], { type: 'message:new', payload: { messageId: message.id, senderOrgId: message.senderOrganizationId, receiverOrgId: message.receiverOrganizationId, type: message.type, }, }) } catch {} return { success: true, message: 'Сообщение отправлено', messageData: message, } } catch (error) { console.error('Error sending message:', error) return { success: false, message: 'Ошибка при отправке сообщения', } } }, // Отправить голосовое сообщение sendVoiceMessage: async ( _: unknown, args: { receiverOrganizationId: string voiceUrl: string voiceDuration: number }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что получатель является контрагентом const isCounterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.receiverOrganizationId, }, }) if (!isCounterparty) { throw new GraphQLError('Можно отправлять сообщения только контрагентам') } // Получаем организацию получателя const receiverOrganization = await prisma.organization.findUnique({ where: { id: args.receiverOrganizationId }, }) if (!receiverOrganization) { throw new GraphQLError('Организация получателя не найдена') } try { // Создаем голосовое сообщение const message = await prisma.message.create({ data: { content: null, type: 'VOICE', voiceUrl: args.voiceUrl, voiceDuration: args.voiceDuration, senderId: context.user.id, senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.receiverOrganizationId, }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, }) try { notifyMany([currentUser.organization.id, args.receiverOrganizationId], { type: 'message:new', payload: { messageId: message.id, senderOrgId: message.senderOrganizationId, receiverOrgId: message.receiverOrganizationId, type: message.type, }, }) } catch {} return { success: true, message: 'Голосовое сообщение отправлено', messageData: message, } } catch (error) { console.error('Error sending voice message:', error) return { success: false, message: 'Ошибка при отправке голосового сообщения', } } }, // Отправить изображение sendImageMessage: async ( _: unknown, args: { receiverOrganizationId: string fileUrl: string fileName: string fileSize: number fileType: string }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что получатель является контрагентом const isCounterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.receiverOrganizationId, }, }) if (!isCounterparty) { throw new GraphQLError('Можно отправлять сообщения только контрагентам') } try { const message = await prisma.message.create({ data: { content: null, type: 'IMAGE', fileUrl: args.fileUrl, fileName: args.fileName, fileSize: args.fileSize, fileType: args.fileType, senderId: context.user.id, senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.receiverOrganizationId, }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, }) try { notifyMany([currentUser.organization.id, args.receiverOrganizationId], { type: 'message:new', payload: { messageId: message.id, senderOrgId: message.senderOrganizationId, receiverOrgId: message.receiverOrganizationId, type: message.type, }, }) } catch {} return { success: true, message: 'Изображение отправлено', messageData: message, } } catch (error) { console.error('Error sending image:', error) return { success: false, message: 'Ошибка при отправке изображения', } } }, // Отправить файл sendFileMessage: async ( _: unknown, args: { receiverOrganizationId: string fileUrl: string fileName: string fileSize: number fileType: string }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что получатель является контрагентом const isCounterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.receiverOrganizationId, }, }) if (!isCounterparty) { throw new GraphQLError('Можно отправлять сообщения только контрагентам') } try { const message = await prisma.message.create({ data: { content: null, type: 'FILE', fileUrl: args.fileUrl, fileName: args.fileName, fileSize: args.fileSize, fileType: args.fileType, senderId: context.user.id, senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.receiverOrganizationId, }, include: { sender: true, senderOrganization: { include: { users: true, }, }, receiverOrganization: { include: { users: true, }, }, }, }) try { notifyMany([currentUser.organization.id, args.receiverOrganizationId], { type: 'message:new', payload: { messageId: message.id, senderOrgId: message.senderOrganizationId, receiverOrgId: message.receiverOrganizationId, type: message.type, }, }) } catch {} return { success: true, message: 'Файл отправлен', messageData: message, } } catch (error) { console.error('Error sending file:', error) return { success: false, message: 'Ошибка при отправке файла', } } }, // Отметить сообщения как прочитанные markMessagesAsRead: async (_: unknown, args: { conversationId: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // conversationId имеет формат "currentOrgId-counterpartyId" const [, counterpartyId] = args.conversationId.split('-') if (!counterpartyId) { throw new GraphQLError('Неверный ID беседы') } // Помечаем все непрочитанные сообщения от контрагента как прочитанные await prisma.message.updateMany({ where: { senderOrganizationId: counterpartyId, receiverOrganizationId: currentUser.organization.id, isRead: false, }, data: { isRead: true, }, }) return true }, // Создать услугу createService: async ( _: unknown, args: { input: { name: string description?: string price: number imageUrl?: string } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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('Услуги доступны только для фулфилмент центров') } try { const service = await prisma.service.create({ data: { name: args.input.name, description: args.input.description, price: args.input.price, imageUrl: args.input.imageUrl, organizationId: currentUser.organization.id, }, include: { organization: true }, }) return { success: true, message: 'Услуга успешно создана', service, } } catch (error) { console.error('Error creating service:', error) return { success: false, message: 'Ошибка при создании услуги', } } }, // Обновить услугу updateService: async ( _: unknown, args: { id: string input: { name: string description?: string price: number imageUrl?: string } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что услуга принадлежит текущей организации const existingService = await prisma.service.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }) if (!existingService) { throw new GraphQLError('Услуга не найдена или нет доступа') } try { const service = await prisma.service.update({ where: { id: args.id }, data: { name: args.input.name, description: args.input.description, price: args.input.price, imageUrl: args.input.imageUrl, }, include: { organization: true }, }) return { success: true, message: 'Услуга успешно обновлена', service, } } catch (error) { console.error('Error updating service:', error) return { success: false, message: 'Ошибка при обновлении услуги', } } }, // Удалить услугу deleteService: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что услуга принадлежит текущей организации const existingService = await prisma.service.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }) if (!existingService) { throw new GraphQLError('Услуга не найдена или нет доступа') } try { await prisma.service.delete({ where: { id: args.id }, }) return true } catch (error) { console.error('Error deleting service:', error) return false } }, // Обновить цену расходника (новая архитектура - только цену можно редактировать) updateSupplyPrice: async ( _: unknown, args: { id: string input: { pricePerUnit?: number | null } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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('Обновление цен расходников доступно только для фулфилмент центров') } try { // Находим и обновляем расходник const existingSupply = await prisma.supply.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }) if (!existingSupply) { throw new GraphQLError('Расходник не найден') } const updatedSupply = await prisma.supply.update({ where: { id: args.id }, data: { pricePerUnit: args.input.pricePerUnit, // Обновляем цену продажи, НЕ цену закупки updatedAt: new Date(), }, include: { organization: true }, }) // Преобразуем в новый формат для GraphQL const transformedSupply = { id: updatedSupply.id, name: updatedSupply.name, description: updatedSupply.description, pricePerUnit: updatedSupply.price ? parseFloat(updatedSupply.price.toString()) : null, // Конвертируем Decimal в Number unit: updatedSupply.unit || 'шт', imageUrl: updatedSupply.imageUrl, warehouseStock: updatedSupply.currentStock || 0, isAvailable: (updatedSupply.currentStock || 0) > 0, warehouseConsumableId: updatedSupply.id, createdAt: updatedSupply.createdAt, updatedAt: updatedSupply.updatedAt, organization: updatedSupply.organization, } console.warn('🔥 SUPPLY PRICE UPDATED:', { id: transformedSupply.id, name: transformedSupply.name, oldPrice: existingSupply.price, newPrice: transformedSupply.pricePerUnit, }) return { success: true, message: 'Цена расходника успешно обновлена', supply: transformedSupply, } } catch (error) { console.error('Error updating supply price:', error) return { success: false, message: 'Ошибка при обновлении цены расходника', } } }, // Использовать расходники фулфилмента useFulfillmentSupplies: async ( _: unknown, args: { input: { supplyId: string quantityUsed: number description?: string } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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 existingSupply = await prisma.supply.findFirst({ where: { id: args.input.supplyId, organizationId: currentUser.organization.id, }, }) if (!existingSupply) { throw new GraphQLError('Расходник не найден или нет доступа') } // Проверяем, что достаточно расходников if (existingSupply.currentStock < args.input.quantityUsed) { throw new GraphQLError( `Недостаточно расходников. Доступно: ${existingSupply.currentStock}, требуется: ${args.input.quantityUsed}`, ) } try { // Обновляем количество расходников const updatedSupply = await prisma.supply.update({ where: { id: args.input.supplyId }, data: { currentStock: existingSupply.currentStock - args.input.quantityUsed, updatedAt: new Date(), }, include: { organization: true }, }) console.warn('🔧 Использованы расходники фулфилмента:', { supplyName: updatedSupply.name, quantityUsed: args.input.quantityUsed, remainingStock: updatedSupply.currentStock, description: args.input.description, }) // Реалтайм: уведомляем о смене складских остатков try { notifyOrganization(currentUser.organization.id, { type: 'warehouse:changed', payload: { supplyId: updatedSupply.id, change: -args.input.quantityUsed }, }) } catch {} return { success: true, message: `Использовано ${args.input.quantityUsed} ${updatedSupply.unit} расходника "${updatedSupply.name}"`, supply: updatedSupply, } } catch (error) { console.error('Error using fulfillment supplies:', error) return { success: false, message: 'Ошибка при использовании расходников', } } }, // Создать заказ поставки расходников // Два сценария: // 1. Селлер → Поставщик → Фулфилмент (селлер заказывает для фулфилмент-центра) // 2. Фулфилмент → Поставщик → Фулфилмент (фулфилмент заказывает для себя) // // Процесс: Заказчик → Поставщик → [Логистика] → Фулфилмент // 1. Заказчик (селлер или фулфилмент) создает заказ у поставщика расходников // 2. Поставщик получает заказ и готовит товары // 3. Логистика транспортирует товары на склад фулфилмента // 4. Фулфилмент принимает товары на склад // 5. Расходники создаются в системе фулфилмент-центра createSupplyOrder: async ( _: unknown, args: { input: { partnerId: string deliveryDate: string fulfillmentCenterId?: string // ID фулфилмент-центра для доставки logisticsPartnerId?: string // ID логистической компании items: Array<{ productId: string quantity: number recipe?: { services?: string[] fulfillmentConsumables?: string[] sellerConsumables?: string[] marketplaceCardId?: string } }> notes?: string // Дополнительные заметки к заказу consumableType?: string // Классификация расходников // Новые поля для многоуровневой системы packagesCount?: number // Количество грузовых мест (заполняет поставщик) volume?: number // Объём товара в м³ (заполняет поставщик) routes?: Array<{ logisticsId?: string // Ссылка на предустановленный маршрут fromLocation: string // Точка забора toLocation: string // Точка доставки fromAddress?: string // Полный адрес забора toAddress?: string // Полный адрес доставки }> } }, context: Context, ) => { console.warn('🚀 CREATE_SUPPLY_ORDER RESOLVER - ВЫЗВАН:', { hasUser: !!context.user, userId: context.user?.id, inputData: args.input, timestamp: new Date().toISOString(), }) if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) console.warn('🔍 Проверка пользователя:', { userId: context.user.id, userFound: !!currentUser, organizationFound: !!currentUser?.organization, organizationType: currentUser?.organization?.type, organizationId: currentUser?.organization?.id, }) if (!currentUser) { throw new GraphQLError('Пользователь не найден') } if (!currentUser.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем тип организации и определяем роль в процессе поставки const allowedTypes = ['FULFILLMENT', 'SELLER', 'LOGIST'] if (!allowedTypes.includes(currentUser.organization.type)) { throw new GraphQLError('Заказы поставок недоступны для данного типа организации') } // Определяем роль организации в процессе поставки const organizationRole = currentUser.organization.type let fulfillmentCenterId = args.input.fulfillmentCenterId // Если заказ создает фулфилмент-центр, он сам является получателем if (organizationRole === 'FULFILLMENT') { fulfillmentCenterId = currentUser.organization.id } // Если указан фулфилмент-центр, проверяем его существование if (fulfillmentCenterId) { const fulfillmentCenter = await prisma.organization.findFirst({ where: { id: fulfillmentCenterId, type: 'FULFILLMENT', }, }) if (!fulfillmentCenter) { return { success: false, message: 'Указанный фулфилмент-центр не найден', } } } // Проверяем, что партнер существует и является поставщиком const partner = await prisma.organization.findFirst({ where: { id: args.input.partnerId, type: 'WHOLESALE', }, }) if (!partner) { return { success: false, message: 'Партнер не найден или не является поставщиком', } } // Проверяем, что партнер является контрагентом const counterparty = await prisma.counterparty.findFirst({ where: { organizationId: currentUser.organization.id, counterpartyId: args.input.partnerId, }, }) if (!counterparty) { return { success: false, message: 'Данная организация не является вашим партнером', } } // Получаем товары для проверки наличия и цен const productIds = args.input.items.map((item) => item.productId) const products = await prisma.product.findMany({ where: { id: { in: productIds }, organizationId: args.input.partnerId, isActive: true, }, }) if (products.length !== productIds.length) { return { success: false, message: 'Некоторые товары не найдены или неактивны', } } // Проверяем наличие товаров for (const item of args.input.items) { const product = products.find((p) => p.id === item.productId) if (!product) { return { success: false, message: `Товар ${item.productId} не найден`, } } if (product.quantity < item.quantity) { return { success: false, message: `Недостаточно товара "${product.name}". Доступно: ${product.quantity}, запрошено: ${item.quantity}`, } } } // Рассчитываем общую сумму и количество let totalAmount = 0 let totalItems = 0 const orderItems = args.input.items.map((item) => { const product = products.find((p) => p.id === item.productId)! const itemTotal = Number(product.price) * item.quantity totalAmount += itemTotal totalItems += item.quantity /* ОТКАТ: Новая логика сохранения рецептур - ЗАКОММЕНТИРОВАНО // Получаем полные данные рецептуры из БД let recipeData = null if (item.recipe && (item.recipe.services?.length || item.recipe.fulfillmentConsumables?.length || item.recipe.sellerConsumables?.length)) { // Получаем услуги фулфилмента const services = item.recipe.services ? await context.prisma.supply.findMany({ where: { id: { in: item.recipe.services } }, select: { id: true, name: true, description: true, pricePerUnit: true } }) : [] // Получаем расходники фулфилмента const fulfillmentConsumables = item.recipe.fulfillmentConsumables ? await context.prisma.supply.findMany({ where: { id: { in: item.recipe.fulfillmentConsumables } }, select: { id: true, name: true, description: true, pricePerUnit: true, unit: true, imageUrl: true } }) : [] // Получаем расходники селлера const sellerConsumables = item.recipe.sellerConsumables ? await context.prisma.supply.findMany({ where: { id: { in: item.recipe.sellerConsumables } }, select: { id: true, name: true, description: true, pricePerUnit: true, unit: true } }) : [] recipeData = { services: services.map(service => ({ id: service.id, name: service.name, description: service.description, price: service.pricePerUnit })), fulfillmentConsumables: fulfillmentConsumables.map(consumable => ({ id: consumable.id, name: consumable.name, description: consumable.description, price: consumable.pricePerUnit, unit: consumable.unit, imageUrl: consumable.imageUrl })), sellerConsumables: sellerConsumables.map(consumable => ({ id: consumable.id, name: consumable.name, description: consumable.description, price: consumable.pricePerUnit, unit: consumable.unit })), marketplaceCardId: item.recipe.marketplaceCardId } } return { productId: item.productId, quantity: item.quantity, price: product.price, totalPrice: new Prisma.Decimal(itemTotal), // Сохраняем полную рецептуру как JSON recipe: recipeData ? JSON.stringify(recipeData) : null, } */ // ВОССТАНОВЛЕННАЯ ОРИГИНАЛЬНАЯ ЛОГИКА: return { productId: item.productId, quantity: item.quantity, price: product.price || 0, // Исправлено: защита от null значения totalPrice: new Prisma.Decimal(itemTotal), // Извлечение данных рецептуры из объекта recipe services: item.recipe?.services || [], fulfillmentConsumables: item.recipe?.fulfillmentConsumables || [], sellerConsumables: item.recipe?.sellerConsumables || [], marketplaceCardId: item.recipe?.marketplaceCardId, } }) try { // Определяем начальный статус в зависимости от роли организации let initialStatus: 'PENDING' | 'CONFIRMED' = 'PENDING' if (organizationRole === 'SELLER') { initialStatus = 'PENDING' // Селлер создает заказ, ждет подтверждения поставщика } else if (organizationRole === 'FULFILLMENT') { initialStatus = 'PENDING' // Фулфилмент заказывает для своего склада } else if (organizationRole === 'LOGIST') { initialStatus = 'CONFIRMED' // Логист может сразу подтверждать заказы } // ИСПРАВЛЕНИЕ: Автоматически определяем тип расходников на основе заказчика const consumableType = currentUser.organization.type === 'SELLER' ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES' console.warn('🔍 Автоматическое определение типа расходников:', { organizationType: currentUser.organization.type, consumableType: consumableType, inputType: args.input.consumableType, // Для отладки }) // Подготавливаем данные для создания заказа const createData: any = { partnerId: args.input.partnerId, deliveryDate: new Date(args.input.deliveryDate), totalAmount: new Prisma.Decimal(totalAmount), totalItems: totalItems, organizationId: currentUser.organization.id, fulfillmentCenterId: fulfillmentCenterId, consumableType: consumableType, // ИСПРАВЛЕНО: используем автоматически определенный тип status: initialStatus, // Новые поля для многоуровневой системы (пока что селлер не может задать эти поля) // packagesCount: args.input.packagesCount || null, // Поле не существует в модели // volume: args.input.volume || null, // Поле не существует в модели // notes: args.input.notes || null, // Поле не существует в модели items: { create: orderItems, }, } // 🔄 ЛОГИСТИКА ОПЦИОНАЛЬНА: добавляем только если передана if (args.input.logisticsPartnerId) { createData.logisticsPartnerId = args.input.logisticsPartnerId } console.warn('🔍 Создаем SupplyOrder с данными:', { hasLogistics: !!args.input.logisticsPartnerId, logisticsId: args.input.logisticsPartnerId, createData: createData, }) const supplyOrder = await prisma.supplyOrder.create({ data: createData, include: { partner: { include: { users: true, }, }, organization: { include: { users: true, }, }, fulfillmentCenter: { include: { users: true, }, }, logisticsPartner: { include: { users: true, }, }, // employee: true, // Поле не существует в модели items: { include: { product: { include: { category: true, organization: true, }, }, }, }, // Маршруты будут добавлены отдельно после создания }, }) // 📍 СОЗДАЕМ МАРШРУТЫ ПОСТАВКИ (если указаны) if (args.input.routes && args.input.routes.length > 0) { const routesData = args.input.routes.map((route) => ({ supplyOrderId: supplyOrder.id, logisticsId: route.logisticsId || null, fromLocation: route.fromLocation, toLocation: route.toLocation, fromAddress: route.fromAddress || null, toAddress: route.toAddress || null, status: 'pending', createdDate: new Date(), // Дата создания маршрута (уровень 2) })) await prisma.supplyRoute.createMany({ data: routesData, }) console.warn(`📍 Созданы маршруты для заказа ${supplyOrder.id}:`, routesData.length) } else { // Создаем маршрут по умолчанию на основе адресов организаций const defaultRoute = { supplyOrderId: supplyOrder.id, fromLocation: partner.market || partner.address || 'Поставщик', toLocation: fulfillmentCenterId ? 'Фулфилмент-центр' : 'Получатель', fromAddress: partner.addressFull || partner.address || null, toAddress: fulfillmentCenterId ? ( await prisma.organization.findUnique({ where: { id: fulfillmentCenterId }, select: { addressFull: true, address: true }, }) )?.addressFull || null : null, status: 'pending', createdDate: new Date(), } await prisma.supplyRoute.create({ data: defaultRoute, }) console.warn(`📍 Создан маршрут по умолчанию для заказа ${supplyOrder.id}`) } // Реалтайм: уведомляем поставщика и вовлеченные стороны о новом заказе try { const orgIds = [ currentUser.organization.id, args.input.partnerId, fulfillmentCenterId || undefined, args.input.logisticsPartnerId || undefined, ].filter(Boolean) as string[] notifyMany(orgIds, { type: 'supply-order:new', payload: { id: supplyOrder.id, organizationId: currentUser.organization.id }, }) } catch {} // 📦 РЕЗЕРВИРУЕМ ТОВАРЫ У ПОСТАВЩИКА // Увеличиваем поле "ordered" для каждого заказанного товара for (const item of args.input.items) { await prisma.product.update({ where: { id: item.productId }, data: { ordered: { increment: item.quantity, }, }, }) } console.warn( `📦 Зарезервированы товары для заказа ${supplyOrder.id}:`, args.input.items.map((item) => `${item.productId}: +${item.quantity} шт.`).join(', '), ) // Проверяем, является ли это первой сделкой организации const isFirstOrder = (await prisma.supplyOrder.count({ where: { organizationId: currentUser.organization.id, id: { not: supplyOrder.id }, }, })) === 0 // Если это первая сделка и организация была приглашена по реферальной ссылке if (isFirstOrder && currentUser.organization.referredById) { try { // Создаем транзакцию на 100 сфер за первую сделку await prisma.referralTransaction.create({ data: { referrerId: currentUser.organization.referredById, referralId: currentUser.organization.id, points: 100, type: 'FIRST_ORDER', description: `Первая сделка реферала ${currentUser.organization.name || currentUser.organization.inn}`, }, }) // Увеличиваем счетчик сфер у реферера await prisma.organization.update({ where: { id: currentUser.organization.referredById }, data: { referralPoints: { increment: 100 } }, }) console.log(`💰 Начислено 100 сфер рефереру за первую сделку организации ${currentUser.organization.id}`) } catch (error) { console.error('Ошибка начисления сфер за первую сделку:', error) // Не прерываем создание заказа из-за ошибки начисления } } // Создаем расходники на основе заказанных товаров // Расходники создаются в организации получателя (фулфилмент-центре) // Определяем тип расходников на основе consumableType const supplyType = args.input.consumableType === 'SELLER_CONSUMABLES' ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES' // Определяем sellerOwnerId для расходников селлеров const sellerOwnerId = supplyType === 'SELLER_CONSUMABLES' ? currentUser.organization!.id : null const suppliesData = args.input.items.map((item) => { const product = products.find((p) => p.id === item.productId)! const productWithCategory = supplyOrder.items.find( (orderItem: { productId: string; product: { category?: { name: string } | null } }) => orderItem.productId === item.productId, )?.product return { name: product.name, article: product.article, // ИСПРАВЛЕНО: Добавляем артикул товара для уникальности description: product.description || `Заказано у ${partner.name}`, price: product.price, // Цена закупки у поставщика quantity: item.quantity, unit: 'шт', category: productWithCategory?.category?.name || 'Расходники', status: 'planned', // Статус "запланировано" (ожидает одобрения поставщиком) date: new Date(args.input.deliveryDate), supplier: partner.name || partner.fullName || 'Не указан', minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток currentStock: 0, // Пока товар не пришел type: supplyType, // ИСПРАВЛЕНО: Добавляем тип расходников sellerOwnerId: sellerOwnerId, // ИСПРАВЛЕНО: Добавляем владельца для расходников селлеров // Расходники создаются в организации получателя (фулфилмент-центре) organizationId: fulfillmentCenterId || currentUser.organization!.id, } }) // Создаем расходники await prisma.supply.createMany({ data: suppliesData, }) // 🔔 ОТПРАВЛЯЕМ УВЕДОМЛЕНИЕ ПОСТАВЩИКУ О НОВОМ ЗАКАЗЕ try { const orderSummary = args.input.items .map((item) => { const product = products.find((p) => p.id === item.productId)! return `${product.name} - ${item.quantity} шт.` }) .join(', ') const notificationMessage = `🔔 Новый заказ поставки от ${ currentUser.organization.name || currentUser.organization.fullName }!\n\nТовары: ${orderSummary}\nДата доставки: ${new Date(args.input.deliveryDate).toLocaleDateString( 'ru-RU', )}\nОбщая сумма: ${totalAmount.toLocaleString( 'ru-RU', )} ₽\n\nПожалуйста, подтвердите заказ в разделе "Поставки".` await prisma.message.create({ data: { content: notificationMessage, type: 'TEXT', senderId: context.user.id, senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.input.partnerId, }, }) console.warn(`✅ Уведомление отправлено поставщику ${partner.name}`) } catch (notificationError) { console.error('❌ Ошибка отправки уведомления:', notificationError) // Не прерываем выполнение, если уведомление не отправилось } // Получаем полные данные заказа с маршрутами для ответа const completeOrder = await prisma.supplyOrder.findUnique({ where: { id: supplyOrder.id }, include: { partner: true, organization: true, fulfillmentCenter: true, logisticsPartner: true, employee: true, routes: { include: { logistics: true, }, }, items: { include: { product: { include: { category: true, organization: true, }, }, }, }, }, }) // Формируем сообщение в зависимости от роли организации let successMessage = '' if (organizationRole === 'SELLER') { successMessage = `Заказ поставки товаров создан! Товары будут доставлены ${ fulfillmentCenterId ? 'на указанный фулфилмент-центр' : 'согласно настройкам' }. Ожидайте подтверждения от поставщика.` } else if (organizationRole === 'FULFILLMENT') { successMessage = 'Заказ поставки товаров создан для вашего склада! Ожидайте подтверждения от поставщика и координации с логистикой.' } else if (organizationRole === 'LOGIST') { successMessage = 'Заказ поставки создан и подтвержден! Координируйте доставку товаров от поставщика на фулфилмент-склад.' } return { success: true, message: successMessage, order: completeOrder, processInfo: { role: organizationRole, supplier: partner.name || partner.fullName, fulfillmentCenter: fulfillmentCenterId, logistics: args.input.logisticsPartnerId, status: initialStatus, }, } } catch (error) { console.error('Error creating supply order:', error) console.error('ДЕТАЛИ ОШИБКИ:', error instanceof Error ? error.message : String(error)) console.error('СТЕК ОШИБКИ:', error instanceof Error ? error.stack : 'No stack') return { success: false, message: `Ошибка при создании заказа поставки: ${error instanceof Error ? error.message : String(error)}`, } } }, // Создать товар createProduct: async ( _: unknown, args: { input: { name: string article: string description?: string price: number pricePerSet?: number quantity: number setQuantity?: number ordered?: number inTransit?: number stock?: number sold?: number type?: 'PRODUCT' | 'CONSUMABLE' categoryId?: string brand?: string color?: string size?: string weight?: number dimensions?: string material?: string images?: string[] mainImage?: string isActive?: boolean } }, context: Context, ) => { console.warn('🆕 CREATE_PRODUCT RESOLVER - ВЫЗВАН:', { hasUser: !!context.user, userId: context.user?.id, inputData: args.input, timestamp: new Date().toISOString(), }) if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что это поставщик if (currentUser.organization.type !== 'WHOLESALE') { throw new GraphQLError('Товары доступны только для поставщиков') } // Проверяем уникальность артикула в рамках организации const existingProduct = await prisma.product.findFirst({ where: { article: args.input.article, organizationId: currentUser.organization.id, }, }) if (existingProduct) { return { success: false, message: 'Товар с таким артикулом уже существует', } } try { console.warn('🛍️ СОЗДАНИЕ ТОВАРА - НАЧАЛО:', { userId: currentUser.id, organizationId: currentUser.organization.id, organizationType: currentUser.organization.type, productData: { name: args.input.name, article: args.input.article, type: args.input.type || 'PRODUCT', isActive: args.input.isActive ?? true, }, }) const product = await prisma.product.create({ data: { name: args.input.name, article: args.input.article, description: args.input.description, price: args.input.price, pricePerSet: args.input.pricePerSet, quantity: args.input.quantity, setQuantity: args.input.setQuantity, ordered: args.input.ordered, inTransit: args.input.inTransit, stock: args.input.stock, sold: args.input.sold, type: args.input.type || 'PRODUCT', categoryId: args.input.categoryId, brand: args.input.brand, color: args.input.color, size: args.input.size, weight: args.input.weight, dimensions: args.input.dimensions, material: args.input.material, images: JSON.stringify(args.input.images || []), mainImage: args.input.mainImage, isActive: args.input.isActive ?? true, organizationId: currentUser.organization.id, }, include: { category: true, organization: true, }, }) console.warn('✅ ТОВАР УСПЕШНО СОЗДАН:', { productId: product.id, name: product.name, article: product.article, type: product.type, isActive: product.isActive, organizationId: product.organizationId, createdAt: product.createdAt, }) return { success: true, message: 'Товар успешно создан', product, } } catch (error) { console.error('Error creating product:', error) return { success: false, message: 'Ошибка при создании товара', } } }, // Обновить товар updateProduct: async ( _: unknown, args: { id: string input: { name: string article: string description?: string price: number pricePerSet?: number quantity: number setQuantity?: number ordered?: number inTransit?: number stock?: number sold?: number type?: 'PRODUCT' | 'CONSUMABLE' categoryId?: string brand?: string color?: string size?: string weight?: number dimensions?: string material?: string images?: string[] mainImage?: string isActive?: boolean } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что товар принадлежит текущей организации const existingProduct = await prisma.product.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }) if (!existingProduct) { throw new GraphQLError('Товар не найден или нет доступа') } // Проверяем уникальность артикула (если он изменился) if (args.input.article !== existingProduct.article) { const duplicateProduct = await prisma.product.findFirst({ where: { article: args.input.article, organizationId: currentUser.organization.id, NOT: { id: args.id }, }, }) if (duplicateProduct) { return { success: false, message: 'Товар с таким артикулом уже существует', } } } try { const product = await prisma.product.update({ where: { id: args.id }, data: { name: args.input.name, article: args.input.article, description: args.input.description, price: args.input.price, pricePerSet: args.input.pricePerSet, quantity: args.input.quantity, setQuantity: args.input.setQuantity, ordered: args.input.ordered, inTransit: args.input.inTransit, stock: args.input.stock, sold: args.input.sold, ...(args.input.type && { type: args.input.type }), categoryId: args.input.categoryId, brand: args.input.brand, color: args.input.color, size: args.input.size, weight: args.input.weight, dimensions: args.input.dimensions, material: args.input.material, images: args.input.images ? JSON.stringify(args.input.images) : undefined, mainImage: args.input.mainImage, isActive: args.input.isActive ?? true, }, include: { category: true, organization: true, }, }) return { success: true, message: 'Товар успешно обновлен', product, } } catch (error) { console.error('Error updating product:', error) return { success: false, message: 'Ошибка при обновлении товара', } } }, // Проверка уникальности артикула checkArticleUniqueness: async (_: unknown, args: { article: string; excludeId?: string }, context: Context) => { const { currentUser, prisma } = context if (!currentUser?.organization?.id) { return { isUnique: false, existingProduct: null, } } try { const existingProduct = await prisma.product.findFirst({ where: { article: args.article, organizationId: currentUser.organization.id, ...(args.excludeId && { id: { not: args.excludeId } }), }, select: { id: true, name: true, article: true, }, }) return { isUnique: !existingProduct, existingProduct, } } catch (error) { console.error('Error checking article uniqueness:', error) return { isUnique: false, existingProduct: null, } } }, // Резервирование товара при создании заказа reserveProductStock: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => { const { currentUser, prisma } = context if (!currentUser?.organization?.id) { return { success: false, message: 'Необходимо авторизоваться', } } try { const product = await prisma.product.findUnique({ where: { id: args.productId }, }) if (!product) { return { success: false, message: 'Товар не найден', } } // Проверяем доступность товара const availableStock = (product.stock || product.quantity) - (product.ordered || 0) if (availableStock < args.quantity) { return { success: false, message: `Недостаточно товара на складе. Доступно: ${availableStock}, запрошено: ${args.quantity}`, } } // Резервируем товар (увеличиваем поле ordered) const updatedProduct = await prisma.product.update({ where: { id: args.productId }, data: { ordered: (product.ordered || 0) + args.quantity, }, }) console.warn(`📦 Зарезервировано ${args.quantity} единиц товара ${product.name}`) return { success: true, message: `Зарезервировано ${args.quantity} единиц товара`, product: updatedProduct, } } catch (error) { console.error('Error reserving product stock:', error) return { success: false, message: 'Ошибка при резервировании товара', } } }, // Освобождение резерва при отмене заказа releaseProductReserve: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => { const { currentUser, prisma } = context if (!currentUser?.organization?.id) { return { success: false, message: 'Необходимо авторизоваться', } } try { const product = await prisma.product.findUnique({ where: { id: args.productId }, }) if (!product) { return { success: false, message: 'Товар не найден', } } // Освобождаем резерв (уменьшаем поле ordered) const newOrdered = Math.max((product.ordered || 0) - args.quantity, 0) const updatedProduct = await prisma.product.update({ where: { id: args.productId }, data: { ordered: newOrdered, }, }) console.warn(`🔄 Освобожден резерв ${args.quantity} единиц товара ${product.name}`) return { success: true, message: `Освобожден резерв ${args.quantity} единиц товара`, product: updatedProduct, } } catch (error) { console.error('Error releasing product reserve:', error) return { success: false, message: 'Ошибка при освобождении резерва', } } }, // Обновление статуса "в пути" updateProductInTransit: async ( _: unknown, args: { productId: string; quantity: number; operation: string }, context: Context, ) => { const { currentUser, prisma } = context if (!currentUser?.organization?.id) { return { success: false, message: 'Необходимо авторизоваться', } } try { const product = await prisma.product.findUnique({ where: { id: args.productId }, }) if (!product) { return { success: false, message: 'Товар не найден', } } let newInTransit = product.inTransit || 0 let newOrdered = product.ordered || 0 if (args.operation === 'ship') { // При отгрузке: переводим из "заказано" в "в пути" newInTransit = (product.inTransit || 0) + args.quantity newOrdered = Math.max((product.ordered || 0) - args.quantity, 0) } else if (args.operation === 'deliver') { // При доставке: убираем из "в пути", добавляем в "продано" newInTransit = Math.max((product.inTransit || 0) - args.quantity, 0) } const updatedProduct = await prisma.product.update({ where: { id: args.productId }, data: { inTransit: newInTransit, ordered: newOrdered, ...(args.operation === 'deliver' && { sold: (product.sold || 0) + args.quantity, }), }, }) console.warn(`🚚 Обновлен статус "в пути" для товара ${product.name}: ${args.operation}`) return { success: true, message: `Статус товара обновлен: ${args.operation}`, product: updatedProduct, } } catch (error) { console.error('Error updating product in transit:', error) return { success: false, message: 'Ошибка при обновлении статуса товара', } } }, // Удалить товар deleteProduct: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что товар принадлежит текущей организации const existingProduct = await prisma.product.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }) if (!existingProduct) { throw new GraphQLError('Товар не найден или нет доступа') } try { await prisma.product.delete({ where: { id: args.id }, }) return true } catch (error) { console.error('Error deleting product:', error) return false } }, // Создать категорию createCategory: async (_: unknown, args: { input: { name: string } }, context: Context) => { if (!context.user && !context.admin) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } // Проверяем уникальность названия категории const existingCategory = await prisma.category.findUnique({ where: { name: args.input.name }, }) if (existingCategory) { return { success: false, message: 'Категория с таким названием уже существует', } } try { const category = await prisma.category.create({ data: { name: args.input.name, }, }) return { success: true, message: 'Категория успешно создана', category, } } catch (error) { console.error('Error creating category:', error) return { success: false, message: 'Ошибка при создании категории', } } }, // Обновить категорию updateCategory: async (_: unknown, args: { id: string; input: { name: string } }, context: Context) => { if (!context.user && !context.admin) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } // Проверяем существование категории const existingCategory = await prisma.category.findUnique({ where: { id: args.id }, }) if (!existingCategory) { return { success: false, message: 'Категория не найдена', } } // Проверяем уникальность нового названия (если изменилось) if (args.input.name !== existingCategory.name) { const duplicateCategory = await prisma.category.findUnique({ where: { name: args.input.name }, }) if (duplicateCategory) { return { success: false, message: 'Категория с таким названием уже существует', } } } try { const category = await prisma.category.update({ where: { id: args.id }, data: { name: args.input.name, }, }) return { success: true, message: 'Категория успешно обновлена', category, } } catch (error) { console.error('Error updating category:', error) return { success: false, message: 'Ошибка при обновлении категории', } } }, // Удалить категорию deleteCategory: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user && !context.admin) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } // Проверяем существование категории const existingCategory = await prisma.category.findUnique({ where: { id: args.id }, include: { products: true }, }) if (!existingCategory) { throw new GraphQLError('Категория не найдена') } // Проверяем, есть ли товары в этой категории if (existingCategory.products.length > 0) { throw new GraphQLError('Нельзя удалить категорию, в которой есть товары') } try { await prisma.category.delete({ where: { id: args.id }, }) return true } catch (error) { console.error('Error deleting category:', error) return false } }, // Добавить товар в корзину addToCart: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что товар существует и активен const product = await prisma.product.findFirst({ where: { id: args.productId, isActive: true, }, include: { organization: true, }, }) if (!product) { return { success: false, message: 'Товар не найден или неактивен', } } // Проверяем, что пользователь не пытается добавить свой собственный товар if (product.organizationId === currentUser.organization.id) { return { success: false, message: 'Нельзя добавлять собственные товары в корзину', } } // Найти или создать корзину let cart = await prisma.cart.findUnique({ where: { organizationId: currentUser.organization.id }, }) if (!cart) { cart = await prisma.cart.create({ data: { organizationId: currentUser.organization.id, }, }) } try { // Проверяем, есть ли уже такой товар в корзине const existingCartItem = await prisma.cartItem.findUnique({ where: { cartId_productId: { cartId: cart.id, productId: args.productId, }, }, }) if (existingCartItem) { // Обновляем количество const newQuantity = existingCartItem.quantity + args.quantity if (newQuantity > product.quantity) { return { success: false, message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`, } } await prisma.cartItem.update({ where: { id: existingCartItem.id }, data: { quantity: newQuantity }, }) } else { // Создаем новый элемент корзины if (args.quantity > product.quantity) { return { success: false, message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`, } } await prisma.cartItem.create({ data: { cartId: cart.id, productId: args.productId, quantity: args.quantity, }, }) } // Возвращаем обновленную корзину const updatedCart = await prisma.cart.findUnique({ where: { id: cart.id }, include: { items: { include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, }, organization: true, }, }) return { success: true, message: 'Товар добавлен в корзину', cart: updatedCart, } } catch (error) { console.error('Error adding to cart:', error) return { success: false, message: 'Ошибка при добавлении в корзину', } } }, // Обновить количество товара в корзине updateCartItem: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } const cart = await prisma.cart.findUnique({ where: { organizationId: currentUser.organization.id }, }) if (!cart) { return { success: false, message: 'Корзина не найдена', } } // Проверяем, что товар существует в корзине const cartItem = await prisma.cartItem.findUnique({ where: { cartId_productId: { cartId: cart.id, productId: args.productId, }, }, include: { product: true, }, }) if (!cartItem) { return { success: false, message: 'Товар не найден в корзине', } } if (args.quantity <= 0) { return { success: false, message: 'Количество должно быть больше 0', } } if (args.quantity > cartItem.product.quantity) { return { success: false, message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}`, } } try { await prisma.cartItem.update({ where: { id: cartItem.id }, data: { quantity: args.quantity }, }) // Возвращаем обновленную корзину const updatedCart = await prisma.cart.findUnique({ where: { id: cart.id }, include: { items: { include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, }, organization: true, }, }) return { success: true, message: 'Количество товара обновлено', cart: updatedCart, } } catch (error) { console.error('Error updating cart item:', error) return { success: false, message: 'Ошибка при обновлении корзины', } } }, // Удалить товар из корзины removeFromCart: async (_: unknown, args: { productId: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } const cart = await prisma.cart.findUnique({ where: { organizationId: currentUser.organization.id }, }) if (!cart) { return { success: false, message: 'Корзина не найдена', } } try { await prisma.cartItem.delete({ where: { cartId_productId: { cartId: cart.id, productId: args.productId, }, }, }) // Возвращаем обновленную корзину const updatedCart = await prisma.cart.findUnique({ where: { id: cart.id }, include: { items: { include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, }, organization: true, }, }) return { success: true, message: 'Товар удален из корзины', cart: updatedCart, } } catch (error) { console.error('Error removing from cart:', error) return { success: false, message: 'Ошибка при удалении из корзины', } } }, // Очистить корзину clearCart: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } const cart = await prisma.cart.findUnique({ where: { organizationId: currentUser.organization.id }, }) if (!cart) { return false } try { await prisma.cartItem.deleteMany({ where: { cartId: cart.id }, }) return true } catch (error) { console.error('Error clearing cart:', error) return false } }, // Добавить товар в избранное addToFavorites: async (_: unknown, args: { productId: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // Проверяем, что товар существует и активен const product = await prisma.product.findFirst({ where: { id: args.productId, isActive: true, }, include: { organization: true, }, }) if (!product) { return { success: false, message: 'Товар не найден или неактивен', } } // Проверяем, что пользователь не пытается добавить свой собственный товар if (product.organizationId === currentUser.organization.id) { return { success: false, message: 'Нельзя добавлять собственные товары в избранное', } } try { // Проверяем, есть ли уже такой товар в избранном const existingFavorite = await prisma.favorites.findUnique({ where: { organizationId_productId: { organizationId: currentUser.organization.id, productId: args.productId, }, }, }) if (existingFavorite) { return { success: false, message: 'Товар уже в избранном', } } // Добавляем товар в избранное await prisma.favorites.create({ data: { organizationId: currentUser.organization.id, productId: args.productId, }, }) // Возвращаем обновленный список избранного const favorites = await prisma.favorites.findMany({ where: { organizationId: currentUser.organization.id }, include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, orderBy: { createdAt: 'desc' }, }) return { success: true, message: 'Товар добавлен в избранное', favorites: favorites.map((favorite) => favorite.product), } } catch (error) { console.error('Error adding to favorites:', error) return { success: false, message: 'Ошибка при добавлении в избранное', } } }, // Удалить товар из избранного removeFromFavorites: async (_: unknown, args: { productId: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { // Удаляем товар из избранного await prisma.favorites.deleteMany({ where: { organizationId: currentUser.organization.id, productId: args.productId, }, }) // Возвращаем обновленный список избранного const favorites = await prisma.favorites.findMany({ where: { organizationId: currentUser.organization.id }, include: { product: { include: { category: true, organization: { include: { users: true, }, }, }, }, }, orderBy: { createdAt: 'desc' }, }) return { success: true, message: 'Товар удален из избранного', favorites: favorites.map((favorite) => favorite.product), } } catch (error) { console.error('Error removing from favorites:', error) return { success: false, message: 'Ошибка при удалении из избранного', } } }, // Создать сотрудника createEmployee: async (_: unknown, args: { input: CreateEmployeeInput }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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('Доступно только для фулфилмент центров') } try { const employee = await prisma.employee.create({ data: { ...args.input, organizationId: currentUser.organization.id, birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined, passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined, hireDate: new Date(args.input.hireDate), }, include: { organization: true, }, }) return { success: true, message: 'Сотрудник успешно добавлен', employee, } } catch (error) { console.error('Error creating employee:', error) return { success: false, message: 'Ошибка при создании сотрудника', } } }, // Обновить сотрудника updateEmployee: async (_: unknown, args: { id: string; input: UpdateEmployeeInput }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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('Доступно только для фулфилмент центров') } try { const employee = await prisma.employee.update({ where: { id: args.id, organizationId: currentUser.organization.id, }, data: { ...args.input, birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined, passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined, hireDate: args.input.hireDate ? new Date(args.input.hireDate) : undefined, }, include: { organization: true, }, }) return { success: true, message: 'Сотрудник успешно обновлен', employee, } } catch (error) { console.error('Error updating employee:', error) return { success: false, message: 'Ошибка при обновлении сотрудника', } } }, // Удалить сотрудника deleteEmployee: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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('Доступно только для фулфилмент центров') } try { await prisma.employee.delete({ where: { id: args.id, organizationId: currentUser.organization.id, }, }) return true } catch (error) { console.error('Error deleting employee:', error) return false } }, // Обновить табель сотрудника updateEmployeeSchedule: async (_: unknown, args: { input: UpdateScheduleInput }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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('Доступно только для фулфилмент центров') } try { // Проверяем что сотрудник принадлежит организации const employee = await prisma.employee.findFirst({ where: { id: args.input.employeeId, organizationId: currentUser.organization.id, }, }) if (!employee) { throw new GraphQLError('Сотрудник не найден') } // Создаем или обновляем запись табеля await prisma.employeeSchedule.upsert({ where: { employeeId_date: { employeeId: args.input.employeeId, date: new Date(args.input.date), }, }, create: { employeeId: args.input.employeeId, date: new Date(args.input.date), status: args.input.status, hoursWorked: args.input.hoursWorked, overtimeHours: args.input.overtimeHours, notes: args.input.notes, }, update: { status: args.input.status, hoursWorked: args.input.hoursWorked, overtimeHours: args.input.overtimeHours, notes: args.input.notes, }, }) return true } catch (error) { console.error('Error updating employee schedule:', error) return false } }, // Создать поставку Wildberries createWildberriesSupply: async ( _: unknown, args: { input: { cards: Array<{ price: number discountedPrice?: number selectedQuantity: number selectedServices?: string[] }> } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { // Пока что просто логируем данные, так как таблицы еще нет console.warn('Создание поставки Wildberries с данными:', args.input) const totalAmount = args.input.cards.reduce((sum: number, card) => { const cardPrice = card.discountedPrice || card.price const servicesPrice = (card.selectedServices?.length || 0) * 50 return sum + (cardPrice + servicesPrice) * card.selectedQuantity }, 0) const totalItems = args.input.cards.reduce((sum: number, card) => sum + card.selectedQuantity, 0) // Временная заглушка - вернем success без создания в БД return { success: true, message: `Поставка создана успешно! Товаров: ${totalItems}, Сумма: ${totalAmount} руб.`, supply: null, // Временно null } } catch (error) { console.error('Error creating Wildberries supply:', error) return { success: false, message: 'Ошибка при создании поставки Wildberries', } } }, // Создать поставщика для поставки createSupplySupplier: async ( _: unknown, args: { input: { name: string contactName: string phone: string market?: string address?: string place?: string telegram?: string } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { // Создаем поставщика в базе данных const supplier = await prisma.supplySupplier.create({ data: { name: args.input.name, contactName: args.input.contactName, phone: args.input.phone, market: args.input.market, address: args.input.address, place: args.input.place, telegram: args.input.telegram, organizationId: currentUser.organization.id, }, }) return { success: true, message: 'Поставщик добавлен успешно!', supplier: { id: supplier.id, name: supplier.name, contactName: supplier.contactName, phone: supplier.phone, market: supplier.market, address: supplier.address, place: supplier.place, telegram: supplier.telegram, createdAt: supplier.createdAt, }, } } catch (error) { console.error('Error creating supply supplier:', error) return { success: false, message: 'Ошибка при добавлении поставщика', } } }, // Обновить статус заказа поставки updateSupplyOrderStatus: async ( _: unknown, args: { id: string status: | 'PENDING' | 'CONFIRMED' | 'IN_TRANSIT' | 'SUPPLIER_APPROVED' | 'LOGISTICS_CONFIRMED' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED' }, context: Context, ) => { console.warn(`[DEBUG] updateSupplyOrderStatus вызван для заказа ${args.id} со статусом ${args.status}`) if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { // Находим заказ поставки const existingOrder = await prisma.supplyOrder.findFirst({ where: { id: args.id, OR: [ { organizationId: currentUser.organization.id }, // Создатель заказа { partnerId: currentUser.organization.id }, // Поставщик { fulfillmentCenterId: currentUser.organization.id }, // Фулфилмент-центр ], }, include: { items: { include: { product: { include: { category: true, }, }, }, }, partner: true, fulfillmentCenter: true, }, }) if (!existingOrder) { throw new GraphQLError('Заказ поставки не найден или нет доступа') } // Обновляем статус заказа const updatedOrder = await prisma.supplyOrder.update({ where: { id: args.id }, data: { status: args.status }, include: { partner: true, items: { include: { product: { include: { category: true, }, }, }, }, }, }) // ОТКЛЮЧЕНО: Устаревшая логика для обновления расходников // Теперь используются специальные мутации для каждой роли const targetOrganizationId = existingOrder.fulfillmentCenterId || existingOrder.organizationId if (args.status === 'CONFIRMED') { console.warn(`[WARNING] Попытка использовать устаревший статус CONFIRMED для заказа ${args.id}`) // Не обновляем расходники для устаревших статусов // await prisma.supply.updateMany({ // where: { // organizationId: targetOrganizationId, // status: "planned", // name: { // in: existingOrder.items.map(item => item.product.name) // } // }, // data: { // status: "confirmed" // } // }); console.warn("✅ Статусы расходников обновлены на 'confirmed'") } if (args.status === 'IN_TRANSIT') { // При отгрузке - переводим расходники в статус "in-transit" await prisma.supply.updateMany({ where: { organizationId: targetOrganizationId, status: 'confirmed', name: { in: existingOrder.items.map((item) => item.product.name), }, }, data: { status: 'in-transit', }, }) console.warn("✅ Статусы расходников обновлены на 'in-transit'") } // Если статус изменился на DELIVERED, обновляем склад if (args.status === 'DELIVERED') { console.warn('🚚 Обновляем склад организации:', { targetOrganizationId, fulfillmentCenterId: existingOrder.fulfillmentCenterId, organizationId: existingOrder.organizationId, itemsCount: existingOrder.items.length, items: existingOrder.items.map((item) => ({ productName: item.product.name, quantity: item.quantity, })), }) // 🔄 СИНХРОНИЗАЦИЯ: Обновляем товары поставщика (переводим из "в пути" в "продано" + обновляем основные остатки) for (const item of existingOrder.items) { const product = await prisma.product.findUnique({ where: { id: item.product.id }, }) if (product) { // ИСПРАВЛЕНО: НЕ списываем повторно, только переводим из inTransit в sold // Остаток уже был уменьшен при создании/одобрении заказа await prisma.product.update({ where: { id: item.product.id }, data: { // НЕ ТРОГАЕМ stock - он уже правильно уменьшен при заказе // Только переводим из inTransit в sold inTransit: Math.max((product.inTransit || 0) - item.quantity, 0), sold: (product.sold || 0) + item.quantity, }, }) console.warn( `✅ Товар поставщика "${product.name}" обновлен: доставлено ${ item.quantity } единиц (остаток НЕ ИЗМЕНЕН: ${product.stock || product.quantity || 0})`, ) } } // Обновляем расходники for (const item of existingOrder.items) { console.warn('📦 Обрабатываем товар:', { productName: item.product.name, quantity: item.quantity, targetOrganizationId, consumableType: existingOrder.consumableType, }) // ИСПРАВЛЕНИЕ: Определяем правильный тип расходников const isSellerSupply = existingOrder.consumableType === 'SELLER_CONSUMABLES' const supplyType = isSellerSupply ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES' const sellerOwnerId = isSellerSupply ? existingOrder.organizationId : null console.warn('🔍 Определен тип расходников:', { isSellerSupply, supplyType, sellerOwnerId, }) // ИСПРАВЛЕНИЕ: Ищем по Артикул СФ для уникальности вместо имени const whereCondition = isSellerSupply ? { organizationId: targetOrganizationId, article: item.product.article, // ИЗМЕНЕНО: поиск по article вместо name type: 'SELLER_CONSUMABLES' as const, sellerOwnerId: existingOrder.organizationId, } : { organizationId: targetOrganizationId, article: item.product.article, // ИЗМЕНЕНО: поиск по article вместо name type: 'FULFILLMENT_CONSUMABLES' as const, sellerOwnerId: null, // Для фулфилмента sellerOwnerId должен быть null } console.warn('🔍 Ищем существующий расходник с условиями:', whereCondition) const existingSupply = await prisma.supply.findFirst({ where: whereCondition, }) if (existingSupply) { console.warn('📈 ОБНОВЛЯЕМ существующий расходник:', { id: existingSupply.id, oldStock: existingSupply.currentStock, oldQuantity: existingSupply.quantity, addingQuantity: item.quantity, }) // ОБНОВЛЯЕМ существующий расходник const updatedSupply = await prisma.supply.update({ where: { id: existingSupply.id }, data: { currentStock: existingSupply.currentStock + item.quantity, // ❌ ИСПРАВЛЕНО: НЕ обновляем quantity - это изначальное количество заказа! // quantity остается как было изначально заказано status: 'in-stock', // Меняем статус на "на складе" updatedAt: new Date(), }, }) console.warn('✅ Расходник ОБНОВЛЕН (НЕ создан дубликат):', { id: updatedSupply.id, name: updatedSupply.name, newCurrentStock: updatedSupply.currentStock, newTotalQuantity: updatedSupply.quantity, type: updatedSupply.type, }) } else { console.warn('➕ СОЗДАЕМ новый расходник (не найден существующий):', { name: item.product.name, quantity: item.quantity, organizationId: targetOrganizationId, type: supplyType, sellerOwnerId: sellerOwnerId, }) // СОЗДАЕМ новый расходник const newSupply = await prisma.supply.create({ data: { name: item.product.name, article: item.product.article, // ДОБАВЛЕНО: Артикул СФ для уникальности description: item.product.description || `Поставка от ${existingOrder.partner.name}`, price: item.price, // Цена закупки у поставщика quantity: item.quantity, unit: 'шт', category: item.product.category?.name || 'Расходники', status: 'in-stock', date: new Date(), supplier: existingOrder.partner.name || existingOrder.partner.fullName || 'Не указан', minStock: Math.round(item.quantity * 0.1), currentStock: item.quantity, organizationId: targetOrganizationId, type: supplyType as 'SELLER_CONSUMABLES' | 'FULFILLMENT_CONSUMABLES', sellerOwnerId: sellerOwnerId, }, }) console.warn('✅ Новый расходник СОЗДАН:', { id: newSupply.id, name: newSupply.name, currentStock: newSupply.currentStock, type: newSupply.type, sellerOwnerId: newSupply.sellerOwnerId, }) } } console.warn('🎉 Склад организации успешно обновлен!') } // Уведомляем вовлеченные организации об изменении статуса заказа try { const orgIds = [ existingOrder.organizationId, existingOrder.partnerId, existingOrder.fulfillmentCenterId || undefined, ].filter(Boolean) as string[] notifyMany(orgIds, { type: 'supply-order:updated', payload: { id: updatedOrder.id, status: updatedOrder.status }, }) } catch {} return { success: true, message: `Статус заказа поставки обновлен на "${args.status}"`, order: updatedOrder, } } catch (error) { console.error('Error updating supply order status:', error) return { success: false, message: 'Ошибка при обновлении статуса заказа поставки', } } }, // Обновление параметров поставки (объём и грузовые места) updateSupplyParameters: async ( _: unknown, args: { id: string; volume?: number; packagesCount?: number }, context: GraphQLContext, ) => { try { // Проверка аутентификации if (!context.user?.id) { return { success: false, message: 'Необходима аутентификация', } } // Найти поставку и проверить права доступа const supply = await prisma.supplyOrder.findUnique({ where: { id: args.id }, include: { partner: true }, }) if (!supply) { return { success: false, message: 'Поставка не найдена', } } // Проверить, что пользователь - поставщик этой заявки if (supply.partnerId !== context.user.organization?.id) { return { success: false, message: 'Недостаточно прав для изменения данной поставки', } } // Подготовить данные для обновления const updateData: { volume?: number; packagesCount?: number } = {} if (args.volume !== undefined) updateData.volume = args.volume if (args.packagesCount !== undefined) updateData.packagesCount = args.packagesCount // Обновить поставку const updatedSupply = await prisma.supplyOrder.update({ where: { id: args.id }, data: updateData, }) return { success: true, message: 'Параметры поставки обновлены', order: updatedSupply, } } catch (error) { console.error('Ошибка при обновлении параметров поставки:', error) return { success: false, message: 'Ошибка при обновлении параметров поставки', } } }, // Назначение логистики фулфилментом на заказ селлера assignLogisticsToSupply: async ( _: unknown, args: { supplyOrderId: string logisticsPartnerId: string responsibleId?: string }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } 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('Только фулфилмент может назначать логистику') } try { // Находим заказ const existingOrder = await prisma.supplyOrder.findUnique({ where: { id: args.supplyOrderId }, include: { partner: true, fulfillmentCenter: true, logisticsPartner: true, items: { include: { product: true }, }, }, }) if (!existingOrder) { throw new GraphQLError('Заказ поставки не найден') } // Проверяем, что это заказ для нашего фулфилмент-центра if (existingOrder.fulfillmentCenterId !== currentUser.organization.id) { throw new GraphQLError('Нет доступа к этому заказу') } // Проверяем, что статус позволяет назначить логистику if (existingOrder.status !== 'SUPPLIER_APPROVED') { throw new GraphQLError(`Нельзя назначить логистику для заказа со статусом ${existingOrder.status}`) } // Проверяем, что логистическая компания существует const logisticsPartner = await prisma.organization.findUnique({ where: { id: args.logisticsPartnerId }, }) if (!logisticsPartner || logisticsPartner.type !== 'LOGIST') { throw new GraphQLError('Логистическая компания не найдена') } // Обновляем заказ const updatedOrder = await prisma.supplyOrder.update({ where: { id: args.supplyOrderId }, data: { logisticsPartner: { connect: { id: args.logisticsPartnerId }, }, status: 'CONFIRMED', // Переводим в статус "подтвержден фулфилментом" }, include: { partner: true, fulfillmentCenter: true, logisticsPartner: true, items: { include: { product: true }, }, }, }) console.warn(`✅ Логистика назначена на заказ ${args.supplyOrderId}:`, { logisticsPartner: logisticsPartner.name, responsible: args.responsibleId, newStatus: 'CONFIRMED', }) try { const orgIds = [ existingOrder.organizationId, existingOrder.partnerId, existingOrder.fulfillmentCenterId || undefined, args.logisticsPartnerId, ].filter(Boolean) as string[] notifyMany(orgIds, { type: 'supply-order:updated', payload: { id: updatedOrder.id, status: updatedOrder.status }, }) } catch {} return { success: true, message: 'Логистика успешно назначена', order: updatedOrder, } } catch (error) { console.error('❌ Ошибка при назначении логистики:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка при назначении логистики', } } }, // 🔒 МУТАЦИИ ПОСТАВЩИКА С СИСТЕМОЙ БЕЗОПАСНОСТИ supplierApproveOrder: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА if (currentUser.organization.type !== 'WHOLESALE') { throw new GraphQLError('Доступ разрешен только поставщикам (WHOLESALE)') } try { // 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ const securityContext: SecurityContext = { userId: currentUser.id, organizationId: currentUser.organization.id, organizationType: currentUser.organization.type, userRole: currentUser.organization.type, requestMetadata: { action: 'APPROVE_ORDER', resourceId: args.id, timestamp: new Date().toISOString(), ipAddress: context.req?.ip || 'unknown', userAgent: context.req?.get('user-agent') || 'unknown', }, } // 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ await ParticipantIsolation.validateAccess( prisma, currentUser.organization.id, currentUser.organization.type, 'SUPPLY_ORDER', ) // 🔒 ПОЛУЧЕНИЕ ЗАКАЗА С ПРОВЕРКОЙ ДОСТУПА const existingOrder = await prisma.supplyOrder.findFirst({ where: { id: args.id, partnerId: currentUser.organization.id, // Только поставщик может одобрить status: 'PENDING', // Можно одобрить только заказы в статусе PENDING }, include: { organization: true, partner: true, }, }) if (!existingOrder) { return { success: false, message: 'Заказ не найден или недоступен для одобрения', } } // 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ await ParticipantIsolation.validatePartnerAccess( prisma, currentUser.organization.id, existingOrder.organizationId, ) // 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ await CommercialDataAudit.logAccess(prisma, { userId: currentUser.id, organizationType: currentUser.organization.type, action: 'APPROVE_ORDER', resourceType: 'SUPPLY_ORDER', resourceId: args.id, metadata: { partnerOrganizationId: existingOrder.organizationId, orderValue: existingOrder.totalAmount?.toString() || '0', ...securityContext.requestMetadata, }, }) console.warn(`[DEBUG] Поставщик ${currentUser.organization.name} одобряет заказ ${args.id}`) // 🔄 СИНХРОНИЗАЦИЯ ОСТАТКОВ: Резервируем товары у поставщика const orderWithItems = await prisma.supplyOrder.findUnique({ where: { id: args.id }, include: { items: { include: { product: true, }, }, }, }) if (orderWithItems) { for (const item of orderWithItems.items) { // Резервируем товар (увеличиваем поле ordered) const product = await prisma.product.findUnique({ where: { id: item.product.id }, }) if (product) { const availableStock = (product.stock || product.quantity) - (product.ordered || 0) if (availableStock < item.quantity) { return { success: false, message: `Недостаточно товара "${product.name}" на складе. Доступно: ${availableStock}, требуется: ${item.quantity}`, } } // Согласно правилам: при одобрении заказа остаток должен уменьшиться const currentStock = product.stock || product.quantity || 0 const newStock = Math.max(currentStock - item.quantity, 0) await prisma.product.update({ where: { id: item.product.id }, data: { // Уменьшаем основной остаток (товар зарезервирован для заказа) stock: newStock, quantity: newStock, // Синхронизируем оба поля для совместимости // Увеличиваем количество заказанного (для отслеживания) ordered: (product.ordered || 0) + item.quantity, }, }) console.warn(`📦 Товар "${product.name}" зарезервирован: ${item.quantity} единиц`) console.warn(` 📊 Остаток: ${currentStock} -> ${newStock} (уменьшен на ${item.quantity})`) console.warn(` 📋 Заказано: ${product.ordered || 0} -> ${(product.ordered || 0) + item.quantity}`) } } } const updatedOrder = await prisma.supplyOrder.update({ where: { id: args.id }, data: { status: 'SUPPLIER_APPROVED' }, include: { partner: true, organization: true, fulfillmentCenter: true, logisticsPartner: true, items: { include: { product: { include: { category: true, organization: true, }, }, }, }, }, }) console.warn('[DEBUG] updatedOrder structure:', { id: updatedOrder.id, itemsCount: updatedOrder.items?.length || 0, firstItem: updatedOrder.items?.[0] ? { productId: updatedOrder.items[0].productId, hasProduct: !!updatedOrder.items[0].product, productOrgId: updatedOrder.items[0].product?.organizationId, hasProductOrg: !!updatedOrder.items[0].product?.organization, } : null, currentUserOrgId: currentUser.organization.id, }) // 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser) const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType) console.warn(`[DEBUG] Заказ ${args.id} успешно обновлен до статуса: ${updatedOrder.status}`) console.warn('[DEBUG] filteredOrder:', { hasData: !!filteredOrder.data, dataId: filteredOrder.data?.id, dataKeys: Object.keys(filteredOrder.data || {}), }) try { const orgIds = [ updatedOrder.organizationId, updatedOrder.partnerId, updatedOrder.fulfillmentCenterId || undefined, updatedOrder.logisticsPartnerId || undefined, ].filter(Boolean) as string[] notifyMany(orgIds, { type: 'supply-order:updated', payload: { id: updatedOrder.id, status: updatedOrder.status }, }) } catch {} // Проверка на случай, если фильтрованные данные null if (!filteredOrder.data || !filteredOrder.data.id) { console.error('[ERROR] filteredOrder.data is null or missing id:', filteredOrder) throw new GraphQLError('Filtered order data is invalid') } return { success: true, message: 'Заказ поставки одобрен поставщиком. Товары зарезервированы, остатки обновлены.', order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data) } } catch (error) { console.error('Error approving supply order:', error) return { success: false, message: 'Ошибка при одобрении заказа поставки', } } }, supplierRejectOrder: async (_: unknown, args: { id: string; reason?: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА if (currentUser.organization.type !== 'WHOLESALE') { throw new GraphQLError('Доступ разрешен только поставщикам (WHOLESALE)') } try { // 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ const securityContext: SecurityContext = { userId: currentUser.id, organizationId: currentUser.organization.id, organizationType: currentUser.organization.type, userRole: currentUser.organization.type, requestMetadata: { action: 'REJECT_ORDER', resourceId: args.id, timestamp: new Date().toISOString(), ipAddress: context.req?.ip || 'unknown', userAgent: context.req?.get('user-agent') || 'unknown', }, } // 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ await ParticipantIsolation.validateAccess( prisma, currentUser.organization.id, currentUser.organization.type, 'SUPPLY_ORDER', ) // 🔒 ПОЛУЧЕНИЕ ЗАКАЗА С ПРОВЕРКОЙ ДОСТУПА const existingOrder = await prisma.supplyOrder.findFirst({ where: { id: args.id, partnerId: currentUser.organization.id, status: 'PENDING', }, include: { organization: true, partner: true, }, }) if (!existingOrder) { return { success: false, message: 'Заказ не найден или недоступен для отклонения', } } // 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ await ParticipantIsolation.validatePartnerAccess( prisma, currentUser.organization.id, existingOrder.organizationId, ) // 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ await CommercialDataAudit.logAccess(prisma, { userId: currentUser.id, organizationType: currentUser.organization.type, action: 'REJECT_ORDER', resourceType: 'SUPPLY_ORDER', resourceId: args.id, metadata: { partnerOrganizationId: existingOrder.organizationId, orderValue: existingOrder.totalAmount?.toString() || '0', rejectionReason: args.reason, ...securityContext.requestMetadata, }, }) const updatedOrder = await prisma.supplyOrder.update({ where: { id: args.id }, data: { status: 'CANCELLED' }, include: { partner: true, organization: true, fulfillmentCenter: true, logisticsPartner: true, items: { include: { product: { include: { category: true, organization: true, }, }, }, }, }, }) // 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser) const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType) // 📦 СНИМАЕМ РЕЗЕРВАЦИЮ ПРИ ОТКЛОНЕНИИ // Восстанавливаем остатки и убираем резервацию для каждого отклоненного товара for (const item of updatedOrder.items) { const product = await prisma.product.findUnique({ where: { id: item.productId }, }) if (product) { // Восстанавливаем основные остатки (на случай, если заказ был одобрен, а затем отклонен) const currentStock = product.stock || product.quantity || 0 const restoredStock = currentStock + item.quantity await prisma.product.update({ where: { id: item.productId }, data: { // Восстанавливаем основной остаток stock: restoredStock, quantity: restoredStock, // Уменьшаем количество заказанного ordered: Math.max((product.ordered || 0) - item.quantity, 0), }, }) console.warn( `🔄 Восстановлены остатки товара "${product.name}": ${currentStock} -> ${restoredStock}, ordered: ${ product.ordered } -> ${Math.max((product.ordered || 0) - item.quantity, 0)}`, ) } } console.warn( `📦 Снята резервация при отклонении заказа ${updatedOrder.id}:`, updatedOrder.items.map((item) => `${item.productId}: -${item.quantity} шт.`).join(', '), ) try { const orgIds = [ updatedOrder.organizationId, updatedOrder.partnerId, updatedOrder.fulfillmentCenterId || undefined, updatedOrder.logisticsPartnerId || undefined, ].filter(Boolean) as string[] notifyMany(orgIds, { type: 'supply-order:updated', payload: { id: updatedOrder.id, status: updatedOrder.status }, }) } catch {} return { success: true, message: args.reason ? `Заказ отклонен поставщиком. Причина: ${args.reason}` : 'Заказ отклонен поставщиком', order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data) } } catch (error) { console.error('Error rejecting supply order:', error) return { success: false, message: 'Ошибка при отклонении заказа поставки', } } }, supplierShipOrder: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } // 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА if (currentUser.organization.type !== 'WHOLESALE') { throw new GraphQLError('Доступ разрешен только поставщикам (WHOLESALE)') } try { // 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ const securityContext: SecurityContext = { userId: currentUser.id, organizationId: currentUser.organization.id, organizationType: currentUser.organization.type, userRole: currentUser.organization.type, requestMetadata: { action: 'SHIP_ORDER', resourceId: args.id, timestamp: new Date().toISOString(), ipAddress: context.req?.ip || 'unknown', userAgent: context.req?.get('user-agent') || 'unknown', }, } // 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ await ParticipantIsolation.validateAccess( prisma, currentUser.organization.id, currentUser.organization.type, 'SUPPLY_ORDER', ) // 🔒 ПОЛУЧЕНИЕ ЗАКАЗА С ПРОВЕРКОЙ ДОСТУПА const existingOrder = await prisma.supplyOrder.findFirst({ where: { id: args.id, partnerId: currentUser.organization.id, status: 'LOGISTICS_CONFIRMED', }, include: { organization: true, partner: true, }, }) if (!existingOrder) { return { success: false, message: 'Заказ не найден или недоступен для отправки', } } // 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ await ParticipantIsolation.validatePartnerAccess( prisma, currentUser.organization.id, existingOrder.organizationId, ) // 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ await CommercialDataAudit.logAccess(prisma, { userId: currentUser.id, organizationType: currentUser.organization.type, action: 'SHIP_ORDER', resourceType: 'SUPPLY_ORDER', resourceId: args.id, metadata: { partnerOrganizationId: existingOrder.organizationId, orderValue: existingOrder.totalAmount?.toString() || '0', ...securityContext.requestMetadata, }, }) // 🔄 СИНХРОНИЗАЦИЯ ОСТАТКОВ: Переводим товары из "заказано" в "в пути" const orderWithItems = await prisma.supplyOrder.findUnique({ where: { id: args.id }, include: { items: { include: { product: true, }, }, }, }) if (orderWithItems) { for (const item of orderWithItems.items) { const product = await prisma.product.findUnique({ where: { id: item.product.id }, }) if (product) { await prisma.product.update({ where: { id: item.product.id }, data: { ordered: Math.max((product.ordered || 0) - item.quantity, 0), inTransit: (product.inTransit || 0) + item.quantity, }, }) console.warn(`🚚 Товар "${product.name}" переведен в статус "в пути": ${item.quantity} единиц`) } } } const updatedOrder = await prisma.supplyOrder.update({ where: { id: args.id }, data: { status: 'SHIPPED' }, include: { partner: true, organization: true, fulfillmentCenter: true, logisticsPartner: true, items: { include: { product: { include: { category: true, organization: true, }, }, recipe: { include: { services: true, fulfillmentConsumables: true, sellerConsumables: true, }, }, }, }, }, }) // 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser) const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType) try { const orgIds = [ updatedOrder.organizationId, updatedOrder.partnerId, updatedOrder.fulfillmentCenterId || undefined, updatedOrder.logisticsPartnerId || undefined, ].filter(Boolean) as string[] notifyMany(orgIds, { type: 'supply-order:updated', payload: { id: updatedOrder.id, status: updatedOrder.status }, }) } catch {} return { success: true, message: "Заказ отправлен поставщиком. Товары переведены в статус 'в пути'.", order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data) } } catch (error) { console.error('Error shipping supply order:', error) return { success: false, message: 'Ошибка при отправке заказа поставки', } } }, logisticsConfirmOrder: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { const existingOrder = await prisma.supplyOrder.findFirst({ where: { id: args.id, logisticsPartnerId: currentUser.organization.id, OR: [{ status: 'SUPPLIER_APPROVED' }, { status: 'CONFIRMED' }], }, }) if (!existingOrder) { return { success: false, message: 'Заказ не найден или недоступен для подтверждения логистикой', } } const updatedOrder = await prisma.supplyOrder.update({ where: { id: args.id }, data: { status: 'LOGISTICS_CONFIRMED' }, include: { partner: true, organization: true, fulfillmentCenter: true, logisticsPartner: true, items: { include: { product: { include: { category: true, organization: true, }, }, }, }, }, }) try { const orgIds = [ updatedOrder.organizationId, updatedOrder.partnerId, updatedOrder.fulfillmentCenterId || undefined, updatedOrder.logisticsPartnerId || undefined, ].filter(Boolean) as string[] notifyMany(orgIds, { type: 'supply-order:updated', payload: { id: updatedOrder.id, status: updatedOrder.status }, }) } catch {} return { success: true, message: 'Заказ подтвержден логистической компанией', order: updatedOrder, } } catch (error) { console.error('Error confirming supply order:', error) return { success: false, message: 'Ошибка при подтверждении заказа логистикой', } } }, logisticsRejectOrder: async (_: unknown, args: { id: string; reason?: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { const existingOrder = await prisma.supplyOrder.findFirst({ where: { id: args.id, logisticsPartnerId: currentUser.organization.id, OR: [{ status: 'SUPPLIER_APPROVED' }, { status: 'CONFIRMED' }], }, }) if (!existingOrder) { return { success: false, message: 'Заказ не найден или недоступен для отклонения логистикой', } } const updatedOrder = await prisma.supplyOrder.update({ where: { id: args.id }, data: { status: 'CANCELLED' }, include: { partner: true, organization: true, fulfillmentCenter: true, logisticsPartner: true, items: { include: { product: { include: { category: true, organization: true, }, }, }, }, }, }) try { const orgIds = [ updatedOrder.organizationId, updatedOrder.partnerId, updatedOrder.fulfillmentCenterId || undefined, updatedOrder.logisticsPartnerId || undefined, ].filter(Boolean) as string[] notifyMany(orgIds, { type: 'supply-order:updated', payload: { id: updatedOrder.id, status: updatedOrder.status }, }) } catch {} return { success: true, message: args.reason ? `Заказ отклонен логистической компанией. Причина: ${args.reason}` : 'Заказ отклонен логистической компанией', order: updatedOrder, } } catch (error) { console.error('Error rejecting supply order:', error) return { success: false, message: 'Ошибка при отклонении заказа логистикой', } } }, fulfillmentReceiveOrder: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { const existingOrder = await prisma.supplyOrder.findFirst({ where: { id: args.id, fulfillmentCenterId: currentUser.organization.id, status: 'SHIPPED', }, include: { items: { include: { product: { include: { category: true, }, }, }, }, organization: true, // Селлер-создатель заказа partner: true, // Поставщик }, }) if (!existingOrder) { return { success: false, message: 'Заказ не найден или недоступен для приема', } } // Обновляем статус заказа const updatedOrder = await prisma.supplyOrder.update({ where: { id: args.id }, data: { status: 'DELIVERED' }, include: { partner: true, organization: true, fulfillmentCenter: true, logisticsPartner: true, items: { include: { product: { include: { category: true, organization: true, }, }, }, }, }, }) // 🔄 СИНХРОНИЗАЦИЯ СКЛАДА ПОСТАВЩИКА: Обновляем остатки поставщика согласно правилам console.warn('🔄 Начинаем синхронизацию остатков поставщика...') for (const item of existingOrder.items) { const product = await prisma.product.findUnique({ where: { id: item.product.id }, }) if (product) { // ИСПРАВЛЕНО: НЕ списываем повторно, только переводим из inTransit в sold // Остаток уже был уменьшен при создании/одобрении заказа await prisma.product.update({ where: { id: item.product.id }, data: { // НЕ ТРОГАЕМ stock - он уже правильно уменьшен при заказе // Только переводим из inTransit в sold inTransit: Math.max((product.inTransit || 0) - item.quantity, 0), sold: (product.sold || 0) + item.quantity, }, }) console.warn(`✅ Товар поставщика "${product.name}" обновлен: получено ${item.quantity} единиц`) console.warn( ` 📊 Остаток: ${product.stock || product.quantity || 0} (НЕ ИЗМЕНЕН - уже списан при заказе)`, ) console.warn( ` 🚚 В пути: ${product.inTransit || 0} -> ${Math.max( (product.inTransit || 0) - item.quantity, 0, )} (УБЫЛО: ${item.quantity})`, ) console.warn( ` 💰 Продано: ${product.sold || 0} -> ${ (product.sold || 0) + item.quantity } (ПРИБЫЛО: ${item.quantity})`, ) } } // Обновляем склад фулфилмента с учетом типа расходников console.warn('📦 Обновляем склад фулфилмента...') console.warn(`🏷️ Тип поставки: ${existingOrder.consumableType || 'FULFILLMENT_CONSUMABLES'}`) for (const item of existingOrder.items) { // Определяем тип расходников и владельца const isSellerSupply = existingOrder.consumableType === 'SELLER_CONSUMABLES' const supplyType = isSellerSupply ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES' const sellerOwnerId = isSellerSupply ? updatedOrder.organization?.id : null // Для расходников селлеров ищем по Артикул СФ И по владельцу const whereCondition = isSellerSupply ? { organizationId: currentUser.organization.id, article: item.product.article, // ИЗМЕНЕНО: поиск по article вместо name type: 'SELLER_CONSUMABLES' as const, sellerOwnerId: sellerOwnerId, } : { organizationId: currentUser.organization.id, article: item.product.article, // ИЗМЕНЕНО: поиск по article вместо name type: 'FULFILLMENT_CONSUMABLES' as const, } const existingSupply = await prisma.supply.findFirst({ where: whereCondition, }) if (existingSupply) { await prisma.supply.update({ where: { id: existingSupply.id }, data: { currentStock: existingSupply.currentStock + item.quantity, // ❌ ИСПРАВЛЕНО: НЕ обновляем quantity - это изначальное количество заказа! status: 'in-stock', }, }) console.warn( `📈 Обновлен существующий ${ isSellerSupply ? 'расходник селлера' : 'расходник фулфилмента' } "${item.product.name}" ${ isSellerSupply ? `(владелец: ${updatedOrder.organization?.name})` : '' }: ${existingSupply.currentStock} -> ${existingSupply.currentStock + item.quantity}`, ) } else { await prisma.supply.create({ data: { name: item.product.name, article: item.product.article, // ДОБАВЛЕНО: Артикул СФ для уникальности description: isSellerSupply ? `Расходники селлера ${updatedOrder.organization?.name || updatedOrder.organization?.fullName}` : item.product.description || `Расходники от ${updatedOrder.partner.name}`, price: item.price, // Цена закупки у поставщика quantity: item.quantity, actualQuantity: item.quantity, // НОВОЕ: Фактически поставленное количество currentStock: item.quantity, usedStock: 0, unit: 'шт', category: item.product.category?.name || 'Расходники', status: 'in-stock', supplier: updatedOrder.partner.name || updatedOrder.partner.fullName || 'Поставщик', type: supplyType as 'SELLER_CONSUMABLES' | 'FULFILLMENT_CONSUMABLES', sellerOwnerId: sellerOwnerId, organizationId: currentUser.organization.id, }, }) console.warn( `➕ Создан новый ${ isSellerSupply ? 'расходник селлера' : 'расходник фулфилмента' } "${item.product.name}" ${ isSellerSupply ? `(владелец: ${updatedOrder.organization?.name})` : '' }: ${item.quantity} единиц`, ) } } console.warn('🎉 Синхронизация склада завершена успешно!') return { success: true, message: 'Заказ принят фулфилментом. Склад обновлен. Остатки поставщика синхронизированы.', order: updatedOrder, } } catch (error) { console.error('Error receiving supply order:', error) return { success: false, message: 'Ошибка при приеме заказа поставки', } } }, updateExternalAdClicks: async (_: unknown, { id, clicks }: { id: string; clicks: number }, 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 existingAd = await prisma.externalAd.findFirst({ where: { id, organizationId: user.organization.id, }, }) if (!existingAd) { throw new GraphQLError('Внешняя реклама не найдена') } await prisma.externalAd.update({ where: { id }, data: { clicks }, }) return { success: true, message: 'Клики успешно обновлены', externalAd: null, } } catch (error) { console.error('Error updating external ad clicks:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка обновления кликов', externalAd: null, } } }, }, // Резолверы типов Organization: { users: async (parent: { id: string; users?: unknown[] }) => { // Если пользователи уже загружены через include, возвращаем их if (parent.users) { return parent.users } // Иначе загружаем отдельно return await prisma.user.findMany({ where: { organizationId: parent.id }, }) }, services: async (parent: { id: string; services?: unknown[] }) => { // Если услуги уже загружены через include, возвращаем их if (parent.services) { return parent.services } // Иначе загружаем отдельно return await prisma.service.findMany({ where: { organizationId: parent.id }, include: { organization: true }, orderBy: { createdAt: 'desc' }, }) }, supplies: async (parent: { id: string; supplies?: unknown[] }) => { // Если расходники уже загружены через include, возвращаем их if (parent.supplies) { return parent.supplies } // Иначе загружаем отдельно return await prisma.supply.findMany({ where: { organizationId: parent.id }, include: { organization: true, sellerOwner: true, // Включаем информацию о селлере-владельце }, orderBy: { createdAt: 'desc' }, }) }, }, Cart: { totalPrice: (parent: { items: Array<{ product: { price: number }; quantity: number }> }) => { return parent.items.reduce((total, item) => { return total + Number(item.product.price) * item.quantity }, 0) }, totalItems: (parent: { items: Array<{ quantity: number }> }) => { return parent.items.reduce((total, item) => total + item.quantity, 0) }, }, CartItem: { totalPrice: (parent: { product: { price: number }; quantity: number }) => { return Number(parent.product.price) * parent.quantity }, isAvailable: (parent: { product: { quantity: number; isActive: boolean }; quantity: number }) => { return parent.product.isActive && parent.product.quantity >= parent.quantity }, availableQuantity: (parent: { product: { quantity: number } }) => { return parent.product.quantity }, }, User: { organization: async (parent: { organizationId?: string; organization?: unknown }) => { // Если организация уже загружена через include, возвращаем её if (parent.organization) { return parent.organization } // Иначе загружаем отдельно если есть organizationId if (parent.organizationId) { return await prisma.organization.findUnique({ where: { id: parent.organizationId }, include: { apiKeys: true, users: true, }, }) } return null }, }, Product: { type: (parent: { type?: string | null }) => parent.type || 'PRODUCT', images: (parent: { images: unknown }) => { // Если images это строка JSON, парсим её в массив if (typeof parent.images === 'string') { try { return JSON.parse(parent.images) } catch { return [] } } // Если это уже массив, возвращаем как есть if (Array.isArray(parent.images)) { return parent.images } // Иначе возвращаем пустой массив return [] }, }, Message: { type: (parent: { type?: string | null }) => { return parent.type || 'TEXT' }, createdAt: (parent: { createdAt: Date | string }) => { if (parent.createdAt instanceof Date) { return parent.createdAt.toISOString() } return parent.createdAt }, updatedAt: (parent: { updatedAt: Date | string }) => { if (parent.updatedAt instanceof Date) { return parent.updatedAt.toISOString() } return parent.updatedAt }, }, Employee: { fullName: (parent: { firstName: string; lastName: string; middleName?: string }) => { const parts = [parent.lastName, parent.firstName] if (parent.middleName) { parts.push(parent.middleName) } return parts.join(' ') }, name: (parent: { firstName: string; lastName: string }) => { return `${parent.firstName} ${parent.lastName}` }, birthDate: (parent: { birthDate?: Date | string | null }) => { if (!parent.birthDate) return null if (parent.birthDate instanceof Date) { return parent.birthDate.toISOString() } return parent.birthDate }, passportDate: (parent: { passportDate?: Date | string | null }) => { if (!parent.passportDate) return null if (parent.passportDate instanceof Date) { return parent.passportDate.toISOString() } return parent.passportDate }, hireDate: (parent: { hireDate: Date | string }) => { if (parent.hireDate instanceof Date) { return parent.hireDate.toISOString() } return parent.hireDate }, createdAt: (parent: { createdAt: Date | string }) => { if (parent.createdAt instanceof Date) { return parent.createdAt.toISOString() } return parent.createdAt }, updatedAt: (parent: { updatedAt: Date | string }) => { if (parent.updatedAt instanceof Date) { return parent.updatedAt.toISOString() } return parent.updatedAt }, }, EmployeeSchedule: { date: (parent: { date: Date | string }) => { if (parent.date instanceof Date) { return parent.date.toISOString() } return parent.date }, createdAt: (parent: { createdAt: Date | string }) => { if (parent.createdAt instanceof Date) { return parent.createdAt.toISOString() } return parent.createdAt }, updatedAt: (parent: { updatedAt: Date | string }) => { if (parent.updatedAt instanceof Date) { return parent.updatedAt.toISOString() } return parent.updatedAt }, employee: async (parent: { employeeId: string }) => { return await prisma.employee.findUnique({ where: { id: parent.employeeId }, }) }, }, } // Мутации для категорий const categoriesMutations = { // Создать категорию createCategory: async (_: unknown, args: { input: { name: string } }) => { try { // Проверяем есть ли уже категория с таким именем const existingCategory = await prisma.category.findUnique({ where: { name: args.input.name }, }) if (existingCategory) { return { success: false, message: 'Категория с таким названием уже существует', } } const category = await prisma.category.create({ data: { name: args.input.name, }, }) return { success: true, message: 'Категория успешно создана', category, } } catch (error) { console.error('Ошибка создания категории:', error) return { success: false, message: 'Ошибка при создании категории', } } }, // Обновить категорию updateCategory: async (_: unknown, args: { id: string; input: { name: string } }) => { try { // Проверяем существует ли категория const existingCategory = await prisma.category.findUnique({ where: { id: args.id }, }) if (!existingCategory) { return { success: false, message: 'Категория не найдена', } } // Проверяем не занято ли имя другой категорией const duplicateCategory = await prisma.category.findFirst({ where: { name: args.input.name, id: { not: args.id }, }, }) if (duplicateCategory) { return { success: false, message: 'Категория с таким названием уже существует', } } const category = await prisma.category.update({ where: { id: args.id }, data: { name: args.input.name, }, }) return { success: true, message: 'Категория успешно обновлена', category, } } catch (error) { console.error('Ошибка обновления категории:', error) return { success: false, message: 'Ошибка при обновлении категории', } } }, // Удалить категорию deleteCategory: async (_: unknown, args: { id: string }) => { try { // Проверяем существует ли категория const existingCategory = await prisma.category.findUnique({ where: { id: args.id }, }) if (!existingCategory) { throw new GraphQLError('Категория не найдена') } // Проверяем есть ли товары в этой категории const productsCount = await prisma.product.count({ where: { categoryId: args.id }, }) if (productsCount > 0) { throw new GraphQLError('Нельзя удалить категорию, в которой есть товары') } await prisma.category.delete({ where: { id: args.id }, }) return true } catch (error) { console.error('Ошибка удаления категории:', error) if (error instanceof GraphQLError) { throw error } throw new GraphQLError('Ошибка при удалении категории') } }, } // Логистические мутации const logisticsMutations = { // Создать логистический маршрут createLogistics: async ( _: unknown, args: { input: { fromLocation: string toLocation: string priceUnder1m3: number priceOver1m3: number description?: string } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { const logistics = await prisma.logistics.create({ data: { fromLocation: args.input.fromLocation, toLocation: args.input.toLocation, priceUnder1m3: args.input.priceUnder1m3, priceOver1m3: args.input.priceOver1m3, description: args.input.description, organizationId: currentUser.organization.id, }, include: { organization: true, }, }) console.warn('✅ Logistics created:', logistics.id) return { success: true, message: 'Логистический маршрут создан', logistics, } } catch (error) { console.error('❌ Error creating logistics:', error) return { success: false, message: 'Ошибка при создании логистического маршрута', } } }, // Обновить логистический маршрут updateLogistics: async ( _: unknown, args: { id: string input: { fromLocation: string toLocation: string priceUnder1m3: number priceOver1m3: number description?: string } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { // Проверяем, что маршрут принадлежит организации пользователя const existingLogistics = await prisma.logistics.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }) if (!existingLogistics) { throw new GraphQLError('Логистический маршрут не найден') } const logistics = await prisma.logistics.update({ where: { id: args.id }, data: { fromLocation: args.input.fromLocation, toLocation: args.input.toLocation, priceUnder1m3: args.input.priceUnder1m3, priceOver1m3: args.input.priceOver1m3, description: args.input.description, }, include: { organization: true, }, }) console.warn('✅ Logistics updated:', logistics.id) return { success: true, message: 'Логистический маршрут обновлен', logistics, } } catch (error) { console.error('❌ Error updating logistics:', error) return { success: false, message: 'Ошибка при обновлении логистического маршрута', } } }, // Удалить логистический маршрут deleteLogistics: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } try { // Проверяем, что маршрут принадлежит организации пользователя const existingLogistics = await prisma.logistics.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }) if (!existingLogistics) { throw new GraphQLError('Логистический маршрут не найден') } await prisma.logistics.delete({ where: { id: args.id }, }) console.warn('✅ Logistics deleted:', args.id) return true } catch (error) { console.error('❌ Error deleting logistics:', error) return false } }, } // Добавляем дополнительные мутации к основным резолверам resolvers.Mutation = { ...resolvers.Mutation, ...categoriesMutations, ...logisticsMutations, } // Админ резолверы const adminQueries = { adminMe: async (_: unknown, __: unknown, context: Context) => { if (!context.admin) { throw new GraphQLError('Требуется авторизация администратора', { extensions: { code: 'UNAUTHENTICATED' }, }) } const admin = await prisma.admin.findUnique({ where: { id: context.admin.id }, }) if (!admin) { throw new GraphQLError('Администратор не найден') } return admin }, allUsers: async (_: unknown, args: { search?: string; limit?: number; offset?: number }, context: Context) => { if (!context.admin) { throw new GraphQLError('Требуется авторизация администратора', { extensions: { code: 'UNAUTHENTICATED' }, }) } const limit = args.limit || 50 const offset = args.offset || 0 // Строим условие поиска const whereCondition: Prisma.UserWhereInput = args.search ? { OR: [ { phone: { contains: args.search, mode: 'insensitive' } }, { managerName: { contains: args.search, mode: 'insensitive' } }, { organization: { OR: [ { name: { contains: args.search, mode: 'insensitive' } }, { fullName: { contains: args.search, mode: 'insensitive' } }, { inn: { contains: args.search, mode: 'insensitive' } }, ], }, }, ], } : {} // Получаем пользователей с пагинацией const [users, total] = await Promise.all([ prisma.user.findMany({ where: whereCondition, include: { organization: true, }, take: limit, skip: offset, orderBy: { createdAt: 'desc' }, }), prisma.user.count({ where: whereCondition, }), ]) return { users, total, hasMore: offset + limit < total, } }, } const adminMutations = { adminLogin: async (_: unknown, args: { username: string; password: string }) => { try { // Найти администратора const admin = await prisma.admin.findUnique({ where: { username: args.username }, }) if (!admin) { return { success: false, message: 'Неверные учетные данные', } } // Проверить активность if (!admin.isActive) { return { success: false, message: 'Аккаунт заблокирован', } } // Проверить пароль const isPasswordValid = await bcrypt.compare(args.password, admin.password) if (!isPasswordValid) { return { success: false, message: 'Неверные учетные данные', } } // Обновить время последнего входа await prisma.admin.update({ where: { id: admin.id }, data: { lastLogin: new Date() }, }) // Создать токен const token = jwt.sign( { adminId: admin.id, username: admin.username, type: 'admin', }, process.env.JWT_SECRET!, { expiresIn: '24h' }, ) return { success: true, message: 'Успешная авторизация', token, admin: { ...admin, password: undefined, // Не возвращаем пароль }, } } catch (error) { console.error('Admin login error:', error) return { success: false, message: 'Ошибка авторизации', } } }, adminLogout: async (_: unknown, __: unknown, context: Context) => { if (!context.admin) { throw new GraphQLError('Требуется авторизация администратора', { extensions: { code: 'UNAUTHENTICATED' }, }) } return true }, } // Wildberries статистика const wildberriesQueries = { debugWildberriesAdverts: 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: { include: { apiKeys: true, }, }, }, }) if (!user?.organization || user.organization.type !== 'SELLER') { throw new GraphQLError('Доступно только для продавцов') } const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive) if (!wbApiKeyRecord) { throw new GraphQLError('WB API ключ не настроен') } const wbService = new WildberriesService(wbApiKeyRecord.apiKey) // Получаем кампании во всех статусах const [active, completed, paused] = await Promise.all([ wbService.getAdverts(9).catch(() => []), // активные wbService.getAdverts(7).catch(() => []), // завершенные wbService.getAdverts(11).catch(() => []), // на паузе ]) const allCampaigns = [...active, ...completed, ...paused] return { success: true, message: `Found ${active.length} active, ${completed.length} completed, ${paused.length} paused campaigns`, campaignsCount: allCampaigns.length, campaigns: allCampaigns.map((c) => ({ id: c.advertId, name: c.name, status: c.status, type: c.type, })), } } catch (error) { console.error('Error debugging WB adverts:', error) return { success: false, message: error instanceof Error ? error.message : 'Unknown error', campaignsCount: 0, campaigns: [], } } }, getWildberriesStatistics: async ( _: unknown, { period, startDate, endDate, }: { period?: 'week' | 'month' | 'quarter' startDate?: string endDate?: string }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } try { // Получаем организацию пользователя и её WB API ключ const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }) if (!user?.organization) { throw new GraphQLError('Организация не найдена') } if (user.organization.type !== 'SELLER') { throw new GraphQLError('Доступно только для продавцов') } const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive) if (!wbApiKeyRecord) { throw new GraphQLError('WB API ключ не настроен') } // Создаем экземпляр сервиса const wbService = new WildberriesService(wbApiKeyRecord.apiKey) // Получаем даты let dateFrom: string let dateTo: string if (startDate && endDate) { // Используем пользовательские даты dateFrom = startDate dateTo = endDate } else if (period) { // Используем предустановленный период dateFrom = WildberriesService.getDatePeriodAgo(period) dateTo = WildberriesService.formatDate(new Date()) } else { throw new GraphQLError('Необходимо указать либо period, либо startDate и endDate') } // Получаем статистику const statistics = await wbService.getStatistics(dateFrom, dateTo) return { success: true, data: statistics, message: null, } } catch (error) { console.error('Error fetching WB statistics:', error) // Фолбэк: пробуем вернуть последние данные из кеша статистики селлера try { const user = await prisma.user.findUnique({ where: { id: context.user!.id }, include: { organization: true }, }) if (user?.organization) { const whereCache: any = { organizationId: user.organization.id, period: startDate && endDate ? 'custom' : (period ?? 'week'), } if (startDate && endDate) { whereCache.dateFrom = new Date(startDate) whereCache.dateTo = new Date(endDate) } const cache = await prisma.sellerStatsCache.findFirst({ where: whereCache, orderBy: { createdAt: 'desc' }, }) if (cache?.productsData) { // Ожидаем, что productsData — строка JSON с полями, сохраненными клиентом const parsed = JSON.parse(cache.productsData as unknown as string) as { tableData?: Array<{ date: string salesUnits: number orders: number advertising: number refusals: number returns: number revenue: number buyoutPercentage: number }> } const table = parsed.tableData ?? [] const dataFromCache = table.map((row) => ({ date: row.date, sales: row.salesUnits, orders: row.orders, advertising: row.advertising, refusals: row.refusals, returns: row.returns, revenue: row.revenue, buyoutPercentage: row.buyoutPercentage, })) if (dataFromCache.length > 0) { return { success: true, data: dataFromCache, message: 'Данные возвращены из кеша из-за ошибки WB API', } } } else if (cache?.advertisingData) { // Fallback №2: если нет productsData, но есть advertisingData — // формируем минимальный набор данных по дням на основе затрат на рекламу try { const adv = JSON.parse(cache.advertisingData as unknown as string) as { dailyData?: Array<{ date: string totalSum?: number totalOrders?: number totalRevenue?: number }> } const daily = adv.dailyData ?? [] const dataFromAdv = daily.map((d) => ({ date: d.date, sales: 0, orders: typeof d.totalOrders === 'number' ? d.totalOrders : 0, advertising: typeof d.totalSum === 'number' ? d.totalSum : 0, refusals: 0, returns: 0, revenue: typeof d.totalRevenue === 'number' ? d.totalRevenue : 0, buyoutPercentage: 0, })) if (dataFromAdv.length > 0) { return { success: true, data: dataFromAdv, message: 'Данные по продажам недоступны из-за ошибки WB API. Показаны данные по рекламе из кеша.', } } } catch (parseErr) { console.error('Failed to parse advertisingData from cache:', parseErr) } } } } catch (fallbackErr) { console.error('Seller stats cache fallback failed:', fallbackErr) } return { success: false, message: error instanceof Error ? error.message : 'Ошибка получения статистики', data: [], } } }, getWildberriesCampaignStats: async ( _: unknown, { input, }: { input: { campaigns: Array<{ id: number dates?: string[] interval?: { begin: string end: string } }> } }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } try { // Получаем организацию пользователя и её WB API ключ const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }) if (!user?.organization) { throw new GraphQLError('Организация не найдена') } if (user.organization.type !== 'SELLER') { throw new GraphQLError('Доступно только для продавцов') } const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive) if (!wbApiKeyRecord) { throw new GraphQLError('WB API ключ не настроен') } // Создаем экземпляр сервиса const wbService = new WildberriesService(wbApiKeyRecord.apiKey) // Преобразуем запросы в нужный формат const requests = input.campaigns.map((campaign) => { if (campaign.dates && campaign.dates.length > 0) { return { id: campaign.id, dates: campaign.dates, } } else if (campaign.interval) { return { id: campaign.id, interval: campaign.interval, } } else { // Если не указаны ни даты, ни интервал, возвращаем данные только за последние сутки return { id: campaign.id, } } }) // Получаем статистику кампаний const campaignStats = await wbService.getCampaignStats(requests) return { success: true, data: campaignStats, message: null, } } catch (error) { console.error('Error fetching WB campaign stats:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка получения статистики кампаний', data: [], } } }, getWildberriesCampaignsList: async (_: unknown, __: unknown, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } try { // Получаем организацию пользователя и её WB API ключ const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: { include: { apiKeys: true, }, }, }, }) if (!user?.organization) { throw new GraphQLError('Организация не найдена') } if (user.organization.type !== 'SELLER') { throw new GraphQLError('Доступно только для продавцов') } const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive) if (!wbApiKeyRecord) { throw new GraphQLError('WB API ключ не настроен') } // Создаем экземпляр сервиса const wbService = new WildberriesService(wbApiKeyRecord.apiKey) // Получаем список кампаний const campaignsList = await wbService.getCampaignsList() return { success: true, data: campaignsList, message: null, } } catch (error) { console.error('Error fetching WB campaigns list:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка получения списка кампаний', data: { adverts: [], all: 0, }, } } }, // Получение заявок покупателей на возврат от Wildberries от всех партнеров-селлеров wbReturnClaims: async ( _: unknown, { isArchive, limit, offset }: { isArchive: boolean; limit?: number; offset?: number }, 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('У пользователя нет организации') } // Проверяем, что это фулфилмент организация if (user.organization.type !== 'FULFILLMENT') { throw new GraphQLError('Доступ только для фулфилмент организаций') } // Получаем всех партнеров-селлеров с активными WB API ключами const partnerSellerOrgs = await prisma.counterparty.findMany({ where: { organizationId: user.organization.id, }, include: { counterparty: { include: { apiKeys: { where: { marketplace: 'WILDBERRIES', isActive: true, }, }, }, }, }, }) // Фильтруем только селлеров с WB API ключами const sellersWithWbKeys = partnerSellerOrgs.filter( (partner) => partner.counterparty.type === 'SELLER' && partner.counterparty.apiKeys.length > 0, ) if (sellersWithWbKeys.length === 0) { return { claims: [], total: 0, } } console.warn(`Found ${sellersWithWbKeys.length} seller partners with WB keys`) // Получаем заявки от всех селлеров параллельно const claimsPromises = sellersWithWbKeys.map(async (partner) => { const wbApiKey = partner.counterparty.apiKeys[0].apiKey const wbService = new WildberriesService(wbApiKey) try { const claimsResponse = await wbService.getClaims({ isArchive, limit: Math.ceil((limit || 50) / sellersWithWbKeys.length), // Распределяем лимит между селлерами offset: 0, }) // Добавляем информацию о селлере к каждой заявке const claimsWithSeller = claimsResponse.claims.map((claim) => ({ ...claim, sellerOrganization: { id: partner.counterparty.id, name: partner.counterparty.name || 'Неизвестная организация', inn: partner.counterparty.inn || '', }, })) console.warn(`Got ${claimsWithSeller.length} claims from seller ${partner.counterparty.name}`) return claimsWithSeller } catch (error) { console.error(`Error fetching claims for seller ${partner.counterparty.name}:`, error) return [] } }) const allClaims = (await Promise.all(claimsPromises)).flat() console.warn(`Total claims aggregated: ${allClaims.length}`) // Сортируем по дате создания (новые первыми) allClaims.sort((a, b) => new Date(b.dt).getTime() - new Date(a.dt).getTime()) // Применяем пагинацию const paginatedClaims = allClaims.slice(offset || 0, (offset || 0) + (limit || 50)) console.warn(`Paginated claims: ${paginatedClaims.length}`) // Преобразуем в формат фронтенда const transformedClaims = paginatedClaims.map((claim) => ({ id: claim.id, claimType: claim.claim_type, status: claim.status, statusEx: claim.status_ex, nmId: claim.nm_id, userComment: claim.user_comment || '', wbComment: claim.wb_comment || null, dt: claim.dt, imtName: claim.imt_name, orderDt: claim.order_dt, dtUpdate: claim.dt_update, photos: claim.photos || [], videoPaths: claim.video_paths || [], actions: claim.actions || [], price: claim.price, currencyCode: claim.currency_code, srid: claim.srid, sellerOrganization: claim.sellerOrganization, })) console.warn(`Returning ${transformedClaims.length} transformed claims to frontend`) return { claims: transformedClaims, total: allClaims.length, } } catch (error) { console.error('Error fetching WB return claims:', error) throw new GraphQLError(error instanceof Error ? error.message : 'Ошибка получения заявок на возврат') } }, } // Резолверы для внешней рекламы const externalAdQueries = { getExternalAds: async (_: unknown, { dateFrom, dateTo }: { dateFrom: string; dateTo: 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 externalAds = await prisma.externalAd.findMany({ where: { organizationId: user.organization.id, date: { gte: new Date(dateFrom), lte: new Date(dateTo + 'T23:59:59.999Z'), }, }, orderBy: { date: 'desc', }, }) return { success: true, message: null, externalAds: externalAds.map((ad) => ({ ...ad, cost: parseFloat(ad.cost.toString()), date: ad.date.toISOString().split('T')[0], createdAt: ad.createdAt.toISOString(), updatedAt: ad.updatedAt.toISOString(), })), } } catch (error) { console.error('Error fetching external ads:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка получения внешней рекламы', externalAds: [], } } }, } const externalAdMutations = { createExternalAd: async ( _: unknown, { input, }: { input: { name: string url: string cost: number date: string nmId: 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 externalAd = await prisma.externalAd.create({ data: { name: input.name, url: input.url, cost: input.cost, date: new Date(input.date), nmId: input.nmId, organizationId: user.organization.id, }, }) return { success: true, message: 'Внешняя реклама успешно создана', externalAd: { ...externalAd, cost: parseFloat(externalAd.cost.toString()), date: externalAd.date.toISOString().split('T')[0], createdAt: externalAd.createdAt.toISOString(), updatedAt: externalAd.updatedAt.toISOString(), }, } } catch (error) { console.error('Error creating external ad:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка создания внешней рекламы', externalAd: null, } } }, updateExternalAd: async ( _: unknown, { id, input, }: { id: string input: { name: string url: string cost: number date: string nmId: 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 existingAd = await prisma.externalAd.findFirst({ where: { id, organizationId: user.organization.id, }, }) if (!existingAd) { throw new GraphQLError('Внешняя реклама не найдена') } const externalAd = await prisma.externalAd.update({ where: { id }, data: { name: input.name, url: input.url, cost: input.cost, date: new Date(input.date), nmId: input.nmId, }, }) return { success: true, message: 'Внешняя реклама успешно обновлена', externalAd: { ...externalAd, cost: parseFloat(externalAd.cost.toString()), date: externalAd.date.toISOString().split('T')[0], createdAt: externalAd.createdAt.toISOString(), updatedAt: externalAd.updatedAt.toISOString(), }, } } catch (error) { console.error('Error updating external ad:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка обновления внешней рекламы', externalAd: null, } } }, deleteExternalAd: async (_: unknown, { id }: { 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 existingAd = await prisma.externalAd.findFirst({ where: { id, organizationId: user.organization.id, }, }) if (!existingAd) { throw new GraphQLError('Внешняя реклама не найдена') } await prisma.externalAd.delete({ where: { id }, }) return { success: true, message: 'Внешняя реклама успешно удалена', externalAd: null, } } catch (error) { console.error('Error deleting external ad:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка удаления внешней рекламы', externalAd: null, } } }, } // Резолверы для кеша склада WB const wbWarehouseCacheQueries = { getWBWarehouseData: 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) { throw new GraphQLError('Организация не найдена') } // Получаем текущую дату без времени const today = new Date() today.setHours(0, 0, 0, 0) // Ищем кеш за сегодня const cache = await prisma.wBWarehouseCache.findFirst({ where: { organizationId: user.organization.id, cacheDate: today, }, orderBy: { createdAt: 'desc', }, }) if (cache) { // Возвращаем данные из кеша return { success: true, message: 'Данные получены из кеша', cache: { ...cache, cacheDate: cache.cacheDate.toISOString().split('T')[0], createdAt: cache.createdAt.toISOString(), updatedAt: cache.updatedAt.toISOString(), }, fromCache: true, } } else { // Кеша нет, нужно загрузить данные из API return { success: true, message: 'Кеш не найден, требуется загрузка из API', cache: null, fromCache: false, } } } catch (error) { console.error('Error getting WB warehouse cache:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка получения кеша склада WB', cache: null, fromCache: false, } } }, } const wbWarehouseCacheMutations = { saveWBWarehouseCache: async ( _: unknown, { input, }: { input: { data: string totalProducts: number totalStocks: number totalReserved: number } }, 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 today = new Date() today.setHours(0, 0, 0, 0) // Используем upsert для создания или обновления кеша const cache = await prisma.wBWarehouseCache.upsert({ where: { organizationId_cacheDate: { organizationId: user.organization.id, cacheDate: today, }, }, update: { data: input.data, totalProducts: input.totalProducts, totalStocks: input.totalStocks, totalReserved: input.totalReserved, }, create: { organizationId: user.organization.id, cacheDate: today, data: input.data, totalProducts: input.totalProducts, totalStocks: input.totalStocks, totalReserved: input.totalReserved, }, }) return { success: true, message: 'Кеш склада WB успешно сохранен', cache: { ...cache, cacheDate: cache.cacheDate.toISOString().split('T')[0], createdAt: cache.createdAt.toISOString(), updatedAt: cache.updatedAt.toISOString(), }, fromCache: false, } } catch (error) { console.error('Error saving WB warehouse cache:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка сохранения кеша склада WB', cache: null, fromCache: false, } } }, } // Добавляем админ запросы и мутации к основным резолверам resolvers.Query = { ...resolvers.Query, ...adminQueries, ...wildberriesQueries, ...externalAdQueries, ...wbWarehouseCacheQueries, // Кеш статистики селлера getSellerStatsCache: async ( _: unknown, args: { period: string; dateFrom?: string | null; dateTo?: string | null }, 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 today = new Date() today.setHours(0, 0, 0, 0) // Для custom учитываем диапазон, иначе только period const where: any = { organizationId: user.organization.id, cacheDate: today, period: args.period, } if (args.period === 'custom') { if (!args.dateFrom || !args.dateTo) { throw new GraphQLError('Для custom необходимо указать dateFrom и dateTo') } where.dateFrom = new Date(args.dateFrom) where.dateTo = new Date(args.dateTo) } const cache = await prisma.sellerStatsCache.findFirst({ where, orderBy: { createdAt: 'desc' }, }) if (!cache) { return { success: true, message: 'Кеш не найден', cache: null, fromCache: false, } } // Если кеш просрочен — не используем его, как и для склада WB (сервер решает, годен ли кеш) const now = new Date() if (cache.expiresAt && cache.expiresAt <= now) { return { success: true, message: 'Кеш устарел, требуется загрузка из API', cache: null, fromCache: false, } } return { success: true, message: 'Данные получены из кеша', cache: { ...cache, cacheDate: cache.cacheDate.toISOString().split('T')[0], dateFrom: cache.dateFrom ? cache.dateFrom.toISOString().split('T')[0] : null, dateTo: cache.dateTo ? cache.dateTo.toISOString().split('T')[0] : null, productsTotalSales: cache.productsTotalSales ? Number(cache.productsTotalSales) : null, advertisingTotalCost: cache.advertisingTotalCost ? Number(cache.advertisingTotalCost) : null, // Возвращаем expiresAt в ISO, чтобы клиент корректно парсил дату expiresAt: cache.expiresAt.toISOString(), createdAt: cache.createdAt.toISOString(), updatedAt: cache.updatedAt.toISOString(), }, fromCache: true, } } catch (error) { console.error('Error getting Seller Stats cache:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка получения кеша статистики', cache: null, fromCache: false, } } }, } resolvers.Mutation = { ...resolvers.Mutation, ...adminMutations, ...externalAdMutations, ...wbWarehouseCacheMutations, // Сохранение кеша статистики селлера saveSellerStatsCache: async ( _: unknown, { input, }: { input: { period: string dateFrom?: string | null dateTo?: string | null productsData?: string | null productsTotalSales?: number | null productsTotalOrders?: number | null productsCount?: number | null advertisingData?: string | null advertisingTotalCost?: number | null advertisingTotalViews?: number | null advertisingTotalClicks?: number | null expiresAt: 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 today = new Date() today.setHours(0, 0, 0, 0) const data: any = { organizationId: user.organization.id, cacheDate: today, period: input.period, dateFrom: input.period === 'custom' && input.dateFrom ? new Date(input.dateFrom) : null, dateTo: input.period === 'custom' && input.dateTo ? new Date(input.dateTo) : null, productsData: input.productsData ?? null, productsTotalSales: input.productsTotalSales ?? null, productsTotalOrders: input.productsTotalOrders ?? null, productsCount: input.productsCount ?? null, advertisingData: input.advertisingData ?? null, advertisingTotalCost: input.advertisingTotalCost ?? null, advertisingTotalViews: input.advertisingTotalViews ?? null, advertisingTotalClicks: input.advertisingTotalClicks ?? null, expiresAt: new Date(input.expiresAt), } // upsert с составным уникальным ключом, содержащим NULL, в Prisma вызывает валидацию. // Делаем вручную: findFirst по уникальному набору, затем update или create. const existing = await prisma.sellerStatsCache.findFirst({ where: { organizationId: user.organization.id, cacheDate: today, period: input.period, dateFrom: data.dateFrom, dateTo: data.dateTo, }, }) const cache = existing ? await prisma.sellerStatsCache.update({ where: { id: existing.id }, data }) : await prisma.sellerStatsCache.create({ data }) return { success: true, message: 'Кеш статистики сохранен', cache: { ...cache, cacheDate: cache.cacheDate.toISOString().split('T')[0], dateFrom: cache.dateFrom ? cache.dateFrom.toISOString().split('T')[0] : null, dateTo: cache.dateTo ? cache.dateTo.toISOString().split('T')[0] : null, productsTotalSales: cache.productsTotalSales ? Number(cache.productsTotalSales) : null, advertisingTotalCost: cache.advertisingTotalCost ? Number(cache.advertisingTotalCost) : null, createdAt: cache.createdAt.toISOString(), updatedAt: cache.updatedAt.toISOString(), }, fromCache: false, } } catch (error) { console.error('Error saving Seller Stats cache:', error) return { success: false, message: error instanceof Error ? error.message : 'Ошибка сохранения кеша статистики', cache: null, fromCache: false, } } }, // Добавляем v2 mutations через spread ...fulfillmentConsumableV2Mutations } /* // Резолвер для парсинга JSON рецептуры в SupplyOrderItem SupplyOrderItem: { recipe: (parent: any) => { // Если recipe это JSON строка, парсим её if (typeof parent.recipe === 'string') { try { return JSON.parse(parent.recipe) } catch (error) { console.error('Error parsing recipe JSON:', error) return null } } // Если recipe уже объект, возвращаем как есть return parent.recipe }, }, */ // =============================================== // НОВАЯ СИСТЕМА ПОСТАВОК V2.0 - RESOLVERS // =============================================== export default resolvers