import jwt from 'jsonwebtoken' import { GraphQLError } from 'graphql' import { GraphQLScalarType, Kind } from 'graphql' import { prisma } from '@/lib/prisma' import { SmsService } from '@/services/sms-service' import { DaDataService } from '@/services/dadata-service' import { MarketplaceService } from '@/services/marketplace-service' import { Prisma } from '@prisma/client' // Сервисы const smsService = new SmsService() const dadataService = new DaDataService() const marketplaceService = new MarketplaceService() // Интерфейсы для типизации interface Context { user?: { id: string phone: 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 } } }) 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, 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) }, // Входящие заявки 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('У пользователя нет организации') } // TODO: Здесь будет логика получения списка чатов // Пока возвращаем пустой массив, так как таблица сообщений еще не создана return [] }, // Мои услуги 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') { throw new GraphQLError('Расходники доступны только для фулфилмент центров') } return await prisma.supply.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true }, orderBy: { createdAt: 'desc' } }) } }, 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.log('verifySmsCode - Generated token:', token ? `${token.substring(0, 20)}...` : 'No token') console.log('verifySmsCode - Full token:', token) console.log('verifySmsCode - User object:', { id: user.id, phone: user.phone }) const result = { success: true, message: 'Авторизация успешна', token, user } console.log('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 } }, context: Context ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' } }) } const { inn } = 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: 'Организация с таким ИНН уже зарегистрирована' } } // Создаем организацию со всеми данными из 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: 'FULFILLMENT', dadataData: JSON.parse(JSON.stringify(organizationData.rawData)) } }) // Привязываем пользователя к организации const updatedUser = await prisma.user.update({ where: { id: context.user.id }, data: { organizationId: organization.id }, include: { organization: { include: { apiKeys: true } } } }) return { success: true, message: 'Фулфилмент организация успешно зарегистрирована', user: updatedUser } } catch (error) { console.error('Error registering fulfillment organization:', error) return { success: false, message: 'Ошибка при регистрации организации' } } }, registerSellerOrganization: async ( _: unknown, args: { input: { phone: string wbApiKey?: string ozonApiKey?: string ozonClientId?: string } }, context: Context ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' } }) } const { wbApiKey, ozonApiKey, ozonClientId } = 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 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' } }) // Добавляем 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 } } } }) return { success: true, message: 'Селлер организация успешно зарегистрирована', user: updatedUser } } catch (error) { console.error('Error registering seller organization:', error) 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 // Валидируем API ключ const validationResult = await marketplaceService.validateApiKey( marketplace, apiKey, clientId ) if (!validationResult.isValid) { return { success: false, message: validationResult.message } } // Если это только валидация, возвращаем результат без сохранения if (validateOnly) { return { success: true, message: 'API ключ действителен', apiKey: { id: 'validate-only', marketplace, 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 } }, 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 } = {} // Название организации больше не обновляется через профиль // Для селлеров устанавливается при регистрации, для остальных - при смене ИНН // Обновляем контактные данные в 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' }] } // Сохраняем дополнительные контакты в 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 } }) 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 } }) ]) } 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 } }, // Отправить сообщение 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 } } } }) 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 } } } }) 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 } } } }) 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 } } } }) 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' } }) } // TODO: Здесь будет логика обновления статуса сообщений // Пока возвращаем успешный ответ 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 } }, // Создать расходник createSupply: async (_: unknown, args: { input: { name: string; description?: string; price: number; quantity: 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 supply = await prisma.supply.create({ data: { name: args.input.name, description: args.input.description, price: args.input.price, quantity: args.input.quantity, imageUrl: args.input.imageUrl, organizationId: currentUser.organization.id }, include: { organization: true } }) return { success: true, message: 'Расходник успешно создан', supply } } catch (error) { console.error('Error creating supply:', error) return { success: false, message: 'Ошибка при создании расходника' } } }, // Обновить расходник updateSupply: async (_: unknown, args: { id: string; input: { name: string; description?: string; price: number; quantity: 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 existingSupply = await prisma.supply.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id } }) if (!existingSupply) { throw new GraphQLError('Расходник не найден или нет доступа') } try { const supply = await prisma.supply.update({ where: { id: args.id }, data: { name: args.input.name, description: args.input.description, price: args.input.price, quantity: args.input.quantity, imageUrl: args.input.imageUrl }, include: { organization: true } }) return { success: true, message: 'Расходник успешно обновлен', supply } } catch (error) { console.error('Error updating supply:', error) return { success: false, message: 'Ошибка при обновлении расходника' } } }, // Удалить расходник deleteSupply: 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 existingSupply = await prisma.supply.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id } }) if (!existingSupply) { throw new GraphQLError('Расходник не найден или нет доступа') } try { await prisma.supply.delete({ where: { id: args.id } }) return true } catch (error) { console.error('Error deleting supply:', error) return false } } }, // Резолверы типов Organization: { users: async (parent: { id: string; users?: unknown[] }) => { // Если пользователи уже загружены через include, возвращаем их if (parent.users) { return parent.users } // Иначе загружаем отдельно return await prisma.user.findMany({ where: { organizationId: parent.id } }) } }, 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 } }, 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 } } }