Добавлены новые зависимости, обновлены стили и улучшена структура проекта. Обновлен README с описанием функционала и технологий. Реализована анимация и адаптивный дизайн. Настроена авторизация с использованием Apollo Client.
This commit is contained in:
358
src/graphql/mutations.ts
Normal file
358
src/graphql/mutations.ts
Normal file
@ -0,0 +1,358 @@
|
||||
import { gql } from 'graphql-tag'
|
||||
|
||||
export const SEND_SMS_CODE = gql`
|
||||
mutation SendSmsCode($phone: String!) {
|
||||
sendSmsCode(phone: $phone) {
|
||||
success
|
||||
message
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const VERIFY_SMS_CODE = gql`
|
||||
mutation VerifySmsCode($phone: String!, $code: String!) {
|
||||
verifySmsCode(phone: $phone, code: $code) {
|
||||
success
|
||||
message
|
||||
token
|
||||
user {
|
||||
id
|
||||
phone
|
||||
organization {
|
||||
id
|
||||
inn
|
||||
kpp
|
||||
name
|
||||
fullName
|
||||
address
|
||||
addressFull
|
||||
ogrn
|
||||
ogrnDate
|
||||
type
|
||||
status
|
||||
actualityDate
|
||||
registrationDate
|
||||
liquidationDate
|
||||
managementName
|
||||
managementPost
|
||||
opfCode
|
||||
opfFull
|
||||
opfShort
|
||||
okato
|
||||
oktmo
|
||||
okpo
|
||||
okved
|
||||
employeeCount
|
||||
revenue
|
||||
taxSystem
|
||||
phones
|
||||
emails
|
||||
apiKeys {
|
||||
id
|
||||
marketplace
|
||||
isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const VERIFY_INN = gql`
|
||||
mutation VerifyInn($inn: String!) {
|
||||
verifyInn(inn: $inn) {
|
||||
success
|
||||
message
|
||||
organization {
|
||||
name
|
||||
fullName
|
||||
address
|
||||
isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const REGISTER_FULFILLMENT_ORGANIZATION = gql`
|
||||
mutation RegisterFulfillmentOrganization($input: FulfillmentRegistrationInput!) {
|
||||
registerFulfillmentOrganization(input: $input) {
|
||||
success
|
||||
message
|
||||
user {
|
||||
id
|
||||
phone
|
||||
organization {
|
||||
id
|
||||
inn
|
||||
kpp
|
||||
name
|
||||
fullName
|
||||
address
|
||||
addressFull
|
||||
ogrn
|
||||
ogrnDate
|
||||
type
|
||||
status
|
||||
actualityDate
|
||||
registrationDate
|
||||
liquidationDate
|
||||
managementName
|
||||
managementPost
|
||||
opfCode
|
||||
opfFull
|
||||
opfShort
|
||||
okato
|
||||
oktmo
|
||||
okpo
|
||||
okved
|
||||
employeeCount
|
||||
revenue
|
||||
taxSystem
|
||||
phones
|
||||
emails
|
||||
apiKeys {
|
||||
id
|
||||
marketplace
|
||||
isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const REGISTER_SELLER_ORGANIZATION = gql`
|
||||
mutation RegisterSellerOrganization($input: SellerRegistrationInput!) {
|
||||
registerSellerOrganization(input: $input) {
|
||||
success
|
||||
message
|
||||
user {
|
||||
id
|
||||
phone
|
||||
organization {
|
||||
id
|
||||
inn
|
||||
kpp
|
||||
name
|
||||
fullName
|
||||
address
|
||||
addressFull
|
||||
ogrn
|
||||
ogrnDate
|
||||
type
|
||||
status
|
||||
actualityDate
|
||||
registrationDate
|
||||
liquidationDate
|
||||
managementName
|
||||
managementPost
|
||||
opfCode
|
||||
opfFull
|
||||
opfShort
|
||||
okato
|
||||
oktmo
|
||||
okpo
|
||||
okved
|
||||
employeeCount
|
||||
revenue
|
||||
taxSystem
|
||||
phones
|
||||
emails
|
||||
apiKeys {
|
||||
id
|
||||
marketplace
|
||||
isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const ADD_MARKETPLACE_API_KEY = gql`
|
||||
mutation AddMarketplaceApiKey($input: MarketplaceApiKeyInput!) {
|
||||
addMarketplaceApiKey(input: $input) {
|
||||
success
|
||||
message
|
||||
apiKey {
|
||||
id
|
||||
marketplace
|
||||
isActive
|
||||
validationData
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const REMOVE_MARKETPLACE_API_KEY = gql`
|
||||
mutation RemoveMarketplaceApiKey($marketplace: MarketplaceType!) {
|
||||
removeMarketplaceApiKey(marketplace: $marketplace)
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_USER_PROFILE = gql`
|
||||
mutation UpdateUserProfile($input: UpdateUserProfileInput!) {
|
||||
updateUserProfile(input: $input) {
|
||||
success
|
||||
message
|
||||
user {
|
||||
id
|
||||
phone
|
||||
organization {
|
||||
id
|
||||
inn
|
||||
kpp
|
||||
name
|
||||
fullName
|
||||
address
|
||||
addressFull
|
||||
ogrn
|
||||
ogrnDate
|
||||
type
|
||||
status
|
||||
actualityDate
|
||||
registrationDate
|
||||
liquidationDate
|
||||
managementName
|
||||
managementPost
|
||||
opfCode
|
||||
opfFull
|
||||
opfShort
|
||||
okato
|
||||
oktmo
|
||||
okpo
|
||||
okved
|
||||
employeeCount
|
||||
revenue
|
||||
taxSystem
|
||||
phones
|
||||
emails
|
||||
apiKeys {
|
||||
id
|
||||
marketplace
|
||||
isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_ORGANIZATION_BY_INN = gql`
|
||||
mutation UpdateOrganizationByInn($inn: String!) {
|
||||
updateOrganizationByInn(inn: $inn) {
|
||||
success
|
||||
message
|
||||
user {
|
||||
id
|
||||
phone
|
||||
organization {
|
||||
id
|
||||
inn
|
||||
kpp
|
||||
name
|
||||
fullName
|
||||
address
|
||||
addressFull
|
||||
ogrn
|
||||
ogrnDate
|
||||
type
|
||||
status
|
||||
actualityDate
|
||||
registrationDate
|
||||
liquidationDate
|
||||
managementName
|
||||
managementPost
|
||||
opfCode
|
||||
opfFull
|
||||
opfShort
|
||||
okato
|
||||
oktmo
|
||||
okpo
|
||||
okved
|
||||
employeeCount
|
||||
revenue
|
||||
taxSystem
|
||||
phones
|
||||
emails
|
||||
apiKeys {
|
||||
id
|
||||
marketplace
|
||||
isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Мутации для контрагентов
|
||||
export const SEND_COUNTERPARTY_REQUEST = gql`
|
||||
mutation SendCounterpartyRequest($organizationId: ID!, $message: String) {
|
||||
sendCounterpartyRequest(organizationId: $organizationId, message: $message) {
|
||||
success
|
||||
message
|
||||
request {
|
||||
id
|
||||
status
|
||||
message
|
||||
createdAt
|
||||
sender {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
}
|
||||
receiver {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const RESPOND_TO_COUNTERPARTY_REQUEST = gql`
|
||||
mutation RespondToCounterpartyRequest($requestId: ID!, $accept: Boolean!) {
|
||||
respondToCounterpartyRequest(requestId: $requestId, accept: $accept) {
|
||||
success
|
||||
message
|
||||
request {
|
||||
id
|
||||
status
|
||||
message
|
||||
createdAt
|
||||
sender {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
}
|
||||
receiver {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const CANCEL_COUNTERPARTY_REQUEST = gql`
|
||||
mutation CancelCounterpartyRequest($requestId: ID!) {
|
||||
cancelCounterpartyRequest(requestId: $requestId)
|
||||
}
|
||||
`
|
||||
|
||||
export const REMOVE_COUNTERPARTY = gql`
|
||||
mutation RemoveCounterparty($organizationId: ID!) {
|
||||
removeCounterparty(organizationId: $organizationId)
|
||||
}
|
||||
`
|
169
src/graphql/queries.ts
Normal file
169
src/graphql/queries.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import { gql } from 'graphql-tag'
|
||||
|
||||
export const GET_ME = gql`
|
||||
query GetMe {
|
||||
me {
|
||||
id
|
||||
phone
|
||||
avatar
|
||||
createdAt
|
||||
organization {
|
||||
id
|
||||
inn
|
||||
kpp
|
||||
name
|
||||
fullName
|
||||
address
|
||||
addressFull
|
||||
ogrn
|
||||
ogrnDate
|
||||
type
|
||||
status
|
||||
actualityDate
|
||||
registrationDate
|
||||
liquidationDate
|
||||
managementName
|
||||
managementPost
|
||||
opfCode
|
||||
opfFull
|
||||
opfShort
|
||||
okato
|
||||
oktmo
|
||||
okpo
|
||||
okved
|
||||
employeeCount
|
||||
revenue
|
||||
taxSystem
|
||||
phones
|
||||
emails
|
||||
apiKeys {
|
||||
id
|
||||
marketplace
|
||||
isActive
|
||||
validationData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Запросы для контрагентов
|
||||
export const SEARCH_ORGANIZATIONS = gql`
|
||||
query SearchOrganizations($type: OrganizationType, $search: String) {
|
||||
searchOrganizations(type: $type, search: $search) {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
address
|
||||
phones
|
||||
emails
|
||||
createdAt
|
||||
isCounterparty
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_MY_COUNTERPARTIES = gql`
|
||||
query GetMyCounterparties {
|
||||
myCounterparties {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
address
|
||||
phones
|
||||
emails
|
||||
createdAt
|
||||
users {
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_INCOMING_REQUESTS = gql`
|
||||
query GetIncomingRequests {
|
||||
incomingRequests {
|
||||
id
|
||||
status
|
||||
message
|
||||
createdAt
|
||||
sender {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
address
|
||||
phones
|
||||
emails
|
||||
}
|
||||
receiver {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_OUTGOING_REQUESTS = gql`
|
||||
query GetOutgoingRequests {
|
||||
outgoingRequests {
|
||||
id
|
||||
status
|
||||
message
|
||||
createdAt
|
||||
sender {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
}
|
||||
receiver {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
type
|
||||
address
|
||||
phones
|
||||
emails
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_ORGANIZATION = gql`
|
||||
query GetOrganization($id: ID!) {
|
||||
organization(id: $id) {
|
||||
id
|
||||
inn
|
||||
name
|
||||
fullName
|
||||
address
|
||||
type
|
||||
apiKeys {
|
||||
id
|
||||
marketplace
|
||||
isActive
|
||||
validationData
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
1363
src/graphql/resolvers.ts
Normal file
1363
src/graphql/resolvers.ts
Normal file
@ -0,0 +1,1363 @@
|
||||
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' })
|
||||
}
|
||||
|
||||
const verifyToken = (token: string): AuthTokenPayload => {
|
||||
try {
|
||||
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload
|
||||
} 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.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)
|
||||
|
||||
const where: any = {
|
||||
id: { not: currentUser.organization.id } // Исключаем только собственную организацию
|
||||
}
|
||||
|
||||
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 к каждой организации
|
||||
return organizations.map(org => ({
|
||||
...org,
|
||||
isCounterparty: existingCounterpartyIds.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' }
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// Создаем организацию селлера - используем название магазина как основное имя
|
||||
const shopName = validationResults[0]?.data?.sellerName || 'Магазин'
|
||||
const organization = await prisma.organization.create({
|
||||
data: {
|
||||
inn: validationResults[0]?.data?.inn || `SELLER_${Date.now()}`,
|
||||
name: shopName,
|
||||
fullName: `Интернет-магазин "${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: 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: 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: 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
|
||||
|
||||
// Обновляем аватар пользователя если указан
|
||||
if (input.avatar) {
|
||||
await prisma.user.update({
|
||||
where: { id: context.user.id },
|
||||
data: { avatar: input.avatar }
|
||||
})
|
||||
}
|
||||
|
||||
// Подготавливаем данные для обновления организации
|
||||
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
|
||||
}
|
||||
} = {}
|
||||
|
||||
if (input.managerName) {
|
||||
customContacts.managerName = input.managerName
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Обновляем организацию
|
||||
const updatedOrganization = 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,
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
// Обновляем организацию
|
||||
const updatedOrganization = 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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Резолверы типов
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
229
src/graphql/typedefs.ts
Normal file
229
src/graphql/typedefs.ts
Normal file
@ -0,0 +1,229 @@
|
||||
import { gql } from 'graphql-tag'
|
||||
|
||||
export const typeDefs = gql`
|
||||
type Query {
|
||||
me: User
|
||||
organization(id: ID!): Organization
|
||||
|
||||
# Поиск организаций по типу для добавления в контрагенты
|
||||
searchOrganizations(type: OrganizationType, search: String): [Organization!]!
|
||||
|
||||
# Мои контрагенты
|
||||
myCounterparties: [Organization!]!
|
||||
|
||||
# Входящие заявки
|
||||
incomingRequests: [CounterpartyRequest!]!
|
||||
|
||||
# Исходящие заявки
|
||||
outgoingRequests: [CounterpartyRequest!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# Авторизация через SMS
|
||||
sendSmsCode(phone: String!): SmsResponse!
|
||||
verifySmsCode(phone: String!, code: String!): AuthResponse!
|
||||
|
||||
# Валидация ИНН
|
||||
verifyInn(inn: String!): InnValidationResponse!
|
||||
|
||||
# Обновление профиля пользователя
|
||||
updateUserProfile(input: UpdateUserProfileInput!): UpdateUserProfileResponse!
|
||||
|
||||
# Обновление данных организации по ИНН
|
||||
updateOrganizationByInn(inn: String!): UpdateOrganizationResponse!
|
||||
|
||||
# Регистрация организации
|
||||
registerFulfillmentOrganization(input: FulfillmentRegistrationInput!): AuthResponse!
|
||||
registerSellerOrganization(input: SellerRegistrationInput!): AuthResponse!
|
||||
|
||||
# Работа с API ключами
|
||||
addMarketplaceApiKey(input: MarketplaceApiKeyInput!): ApiKeyResponse!
|
||||
removeMarketplaceApiKey(marketplace: MarketplaceType!): Boolean!
|
||||
|
||||
# Выход из системы
|
||||
logout: Boolean!
|
||||
|
||||
# Работа с контрагентами
|
||||
sendCounterpartyRequest(organizationId: ID!, message: String): CounterpartyRequestResponse!
|
||||
respondToCounterpartyRequest(requestId: ID!, accept: Boolean!): CounterpartyRequestResponse!
|
||||
cancelCounterpartyRequest(requestId: ID!): Boolean!
|
||||
removeCounterparty(organizationId: ID!): Boolean!
|
||||
}
|
||||
|
||||
# Типы данных
|
||||
type User {
|
||||
id: ID!
|
||||
phone: String!
|
||||
avatar: String
|
||||
organization: Organization
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Organization {
|
||||
id: ID!
|
||||
inn: String!
|
||||
kpp: String
|
||||
name: String
|
||||
fullName: String
|
||||
address: String
|
||||
addressFull: String
|
||||
ogrn: String
|
||||
ogrnDate: String
|
||||
type: OrganizationType!
|
||||
status: String
|
||||
actualityDate: String
|
||||
registrationDate: String
|
||||
liquidationDate: String
|
||||
managementName: String
|
||||
managementPost: String
|
||||
opfCode: String
|
||||
opfFull: String
|
||||
opfShort: String
|
||||
okato: String
|
||||
oktmo: String
|
||||
okpo: String
|
||||
okved: String
|
||||
employeeCount: Int
|
||||
revenue: String
|
||||
taxSystem: String
|
||||
phones: JSON
|
||||
emails: JSON
|
||||
users: [User!]!
|
||||
apiKeys: [ApiKey!]!
|
||||
isCounterparty: Boolean
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type ApiKey {
|
||||
id: ID!
|
||||
marketplace: MarketplaceType!
|
||||
isActive: Boolean!
|
||||
validationData: JSON
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
# Входные типы для мутаций
|
||||
input UpdateUserProfileInput {
|
||||
# Аватар пользователя
|
||||
avatar: String
|
||||
|
||||
# Контактные данные организации
|
||||
orgPhone: String
|
||||
managerName: String
|
||||
telegram: String
|
||||
whatsapp: String
|
||||
email: String
|
||||
|
||||
# Банковские данные
|
||||
bankName: String
|
||||
bik: String
|
||||
accountNumber: String
|
||||
corrAccount: String
|
||||
}
|
||||
|
||||
input FulfillmentRegistrationInput {
|
||||
phone: String!
|
||||
inn: String!
|
||||
}
|
||||
|
||||
input SellerRegistrationInput {
|
||||
phone: String!
|
||||
wbApiKey: String
|
||||
ozonApiKey: String
|
||||
ozonClientId: String
|
||||
}
|
||||
|
||||
input MarketplaceApiKeyInput {
|
||||
marketplace: MarketplaceType!
|
||||
apiKey: String!
|
||||
clientId: String # Для Ozon
|
||||
validateOnly: Boolean # Только валидация без сохранения
|
||||
}
|
||||
|
||||
# Ответные типы
|
||||
type SmsResponse {
|
||||
success: Boolean!
|
||||
message: String!
|
||||
}
|
||||
|
||||
type AuthResponse {
|
||||
success: Boolean!
|
||||
message: String!
|
||||
token: String
|
||||
user: User
|
||||
}
|
||||
|
||||
type InnValidationResponse {
|
||||
success: Boolean!
|
||||
message: String!
|
||||
organization: ValidatedOrganization
|
||||
}
|
||||
|
||||
type ValidatedOrganization {
|
||||
name: String!
|
||||
fullName: String!
|
||||
address: String!
|
||||
isActive: Boolean!
|
||||
}
|
||||
|
||||
type ApiKeyResponse {
|
||||
success: Boolean!
|
||||
message: String!
|
||||
apiKey: ApiKey
|
||||
}
|
||||
|
||||
type UpdateUserProfileResponse {
|
||||
success: Boolean!
|
||||
message: String!
|
||||
user: User
|
||||
}
|
||||
|
||||
type UpdateOrganizationResponse {
|
||||
success: Boolean!
|
||||
message: String!
|
||||
user: User
|
||||
}
|
||||
|
||||
# Enums
|
||||
enum OrganizationType {
|
||||
FULFILLMENT
|
||||
SELLER
|
||||
LOGIST
|
||||
WHOLESALE
|
||||
}
|
||||
|
||||
enum MarketplaceType {
|
||||
WILDBERRIES
|
||||
OZON
|
||||
}
|
||||
|
||||
enum CounterpartyRequestStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
REJECTED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
# Типы для контрагентов
|
||||
type CounterpartyRequest {
|
||||
id: ID!
|
||||
status: CounterpartyRequestStatus!
|
||||
message: String
|
||||
sender: Organization!
|
||||
receiver: Organization!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type CounterpartyRequestResponse {
|
||||
success: Boolean!
|
||||
message: String!
|
||||
request: CounterpartyRequest
|
||||
}
|
||||
|
||||
# JSON скаляр
|
||||
scalar JSON
|
||||
`
|
Reference in New Issue
Block a user