2156 lines
69 KiB
TypeScript
2156 lines
69 KiB
TypeScript
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<string, unknown> = {
|
||
// Больше не исключаем собственную организацию
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
}
|