
- Убран текст "(с рецептурой)" из названий товаров в корзине - Добавлен раздел 9.2.6 в rules-complete.md с единым стандартом корзины - Определены обязательные размеры, структура и функциональность - Запрещено отображение технических суффиксов в UI корзины 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
8197 lines
272 KiB
TypeScript
8197 lines
272 KiB
TypeScript
import { Prisma } from '@prisma/client'
|
||
import bcrypt from 'bcryptjs'
|
||
import { GraphQLError, GraphQLScalarType, Kind } from 'graphql'
|
||
import jwt from 'jsonwebtoken'
|
||
|
||
import { prisma } from '@/lib/prisma'
|
||
import { DaDataService } from '@/services/dadata-service'
|
||
import { MarketplaceService } from '@/services/marketplace-service'
|
||
import { SmsService } from '@/services/sms-service'
|
||
import { WildberriesService } from '@/services/wildberries-service'
|
||
|
||
import '@/lib/seed-init' // Автоматическая инициализация БД
|
||
|
||
// Сервисы
|
||
const smsService = new SmsService()
|
||
const dadataService = new DaDataService()
|
||
const marketplaceService = new MarketplaceService()
|
||
|
||
// Интерфейсы для типизации
|
||
interface Context {
|
||
user?: {
|
||
id: string
|
||
phone: string
|
||
}
|
||
admin?: {
|
||
id: string
|
||
username: string
|
||
}
|
||
}
|
||
|
||
interface CreateEmployeeInput {
|
||
firstName: string
|
||
lastName: string
|
||
middleName?: string
|
||
birthDate?: string
|
||
avatar?: string
|
||
passportPhoto?: string
|
||
passportSeries?: string
|
||
passportNumber?: string
|
||
passportIssued?: string
|
||
passportDate?: string
|
||
address?: string
|
||
position: string
|
||
department?: string
|
||
hireDate: string
|
||
salary?: number
|
||
phone: string
|
||
email?: string
|
||
telegram?: string
|
||
whatsapp?: string
|
||
emergencyContact?: string
|
||
emergencyPhone?: string
|
||
}
|
||
|
||
interface UpdateEmployeeInput {
|
||
firstName?: string
|
||
lastName?: string
|
||
middleName?: string
|
||
birthDate?: string
|
||
avatar?: string
|
||
passportPhoto?: string
|
||
passportSeries?: string
|
||
passportNumber?: string
|
||
passportIssued?: string
|
||
passportDate?: string
|
||
address?: string
|
||
position?: string
|
||
department?: string
|
||
hireDate?: string
|
||
salary?: number
|
||
status?: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED'
|
||
phone?: string
|
||
email?: string
|
||
telegram?: string
|
||
whatsapp?: string
|
||
emergencyContact?: string
|
||
emergencyPhone?: string
|
||
}
|
||
|
||
interface UpdateScheduleInput {
|
||
employeeId: string
|
||
date: string
|
||
status: 'WORK' | 'WEEKEND' | 'VACATION' | 'SICK' | 'ABSENT'
|
||
hoursWorked?: number
|
||
overtimeHours?: number
|
||
notes?: string
|
||
}
|
||
|
||
interface AuthTokenPayload {
|
||
userId: string
|
||
phone: string
|
||
}
|
||
|
||
// JWT утилиты
|
||
const generateToken = (payload: AuthTokenPayload): string => {
|
||
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '30d' })
|
||
}
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
const verifyToken = (token: string): AuthTokenPayload => {
|
||
try {
|
||
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
} catch (error) {
|
||
throw new GraphQLError('Недействительный токен', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
}
|
||
|
||
// Скалярный тип для JSON
|
||
const JSONScalar = new GraphQLScalarType({
|
||
name: 'JSON',
|
||
description: 'JSON custom scalar type',
|
||
serialize(value: unknown) {
|
||
return value // значение отправляется клиенту
|
||
},
|
||
parseValue(value: unknown) {
|
||
return value // значение получено от клиента
|
||
},
|
||
parseLiteral(ast) {
|
||
switch (ast.kind) {
|
||
case Kind.STRING:
|
||
case Kind.BOOLEAN:
|
||
return ast.value
|
||
case Kind.INT:
|
||
case Kind.FLOAT:
|
||
return parseFloat(ast.value)
|
||
case Kind.OBJECT: {
|
||
const value = Object.create(null)
|
||
ast.fields.forEach((field) => {
|
||
value[field.name.value] = parseLiteral(field.value)
|
||
})
|
||
return value
|
||
}
|
||
case Kind.LIST:
|
||
return ast.values.map(parseLiteral)
|
||
default:
|
||
return null
|
||
}
|
||
},
|
||
})
|
||
|
||
// Скалярный тип для DateTime
|
||
const DateTimeScalar = new GraphQLScalarType({
|
||
name: 'DateTime',
|
||
description: 'DateTime custom scalar type',
|
||
serialize(value: unknown) {
|
||
if (value instanceof Date) {
|
||
return value.toISOString() // значение отправляется клиенту как ISO строка
|
||
}
|
||
return value
|
||
},
|
||
parseValue(value: unknown) {
|
||
if (typeof value === 'string') {
|
||
return new Date(value) // значение получено от клиента, парсим как дату
|
||
}
|
||
return value
|
||
},
|
||
parseLiteral(ast) {
|
||
if (ast.kind === Kind.STRING) {
|
||
return new Date(ast.value) // AST значение как дата
|
||
}
|
||
return null
|
||
},
|
||
})
|
||
|
||
function parseLiteral(ast: unknown): unknown {
|
||
const astNode = ast as {
|
||
kind: string
|
||
value?: unknown
|
||
fields?: unknown[]
|
||
values?: unknown[]
|
||
}
|
||
|
||
switch (astNode.kind) {
|
||
case Kind.STRING:
|
||
case Kind.BOOLEAN:
|
||
return astNode.value
|
||
case Kind.INT:
|
||
case Kind.FLOAT:
|
||
return parseFloat(astNode.value as string)
|
||
case Kind.OBJECT: {
|
||
const value = Object.create(null)
|
||
if (astNode.fields) {
|
||
astNode.fields.forEach((field: unknown) => {
|
||
const fieldNode = field as {
|
||
name: { value: string }
|
||
value: unknown
|
||
}
|
||
value[fieldNode.name.value] = parseLiteral(fieldNode.value)
|
||
})
|
||
}
|
||
return value
|
||
}
|
||
case Kind.LIST:
|
||
return (ast as { values: unknown[] }).values.map(parseLiteral)
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
export const resolvers = {
|
||
JSON: JSONScalar,
|
||
DateTime: DateTimeScalar,
|
||
|
||
Query: {
|
||
me: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
return await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
},
|
||
|
||
organization: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const organization = await prisma.organization.findUnique({
|
||
where: { id: args.id },
|
||
include: {
|
||
apiKeys: true,
|
||
users: true,
|
||
},
|
||
})
|
||
|
||
if (!organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Проверяем, что пользователь имеет доступ к этой организации
|
||
const hasAccess = organization.users.some((user) => user.id === context.user!.id)
|
||
if (!hasAccess) {
|
||
throw new GraphQLError('Нет доступа к этой организации', {
|
||
extensions: { code: 'FORBIDDEN' },
|
||
})
|
||
}
|
||
|
||
return organization
|
||
},
|
||
|
||
// Поиск организаций по типу для добавления в контрагенты
|
||
searchOrganizations: async (_: unknown, args: { type?: string; search?: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
// Получаем текущую организацию пользователя
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Получаем уже существующих контрагентов для добавления флага
|
||
const existingCounterparties = await prisma.counterparty.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
select: { counterpartyId: true },
|
||
})
|
||
|
||
const existingCounterpartyIds = existingCounterparties.map((c) => c.counterpartyId)
|
||
|
||
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
|
||
const outgoingRequests = await prisma.counterpartyRequest.findMany({
|
||
where: {
|
||
senderId: currentUser.organization.id,
|
||
status: 'PENDING',
|
||
},
|
||
select: { receiverId: true },
|
||
})
|
||
|
||
const outgoingRequestIds = outgoingRequests.map((r) => r.receiverId)
|
||
|
||
// Получаем входящие заявки для добавления флага hasIncomingRequest
|
||
const incomingRequests = await prisma.counterpartyRequest.findMany({
|
||
where: {
|
||
receiverId: currentUser.organization.id,
|
||
status: 'PENDING',
|
||
},
|
||
select: { senderId: true },
|
||
})
|
||
|
||
const incomingRequestIds = incomingRequests.map((r) => r.senderId)
|
||
|
||
const where: Record<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)
|
||
},
|
||
|
||
// Поставщики поставок
|
||
supplySuppliers: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
const suppliers = await prisma.supplySupplier.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
return suppliers
|
||
},
|
||
|
||
// Логистика конкретной организации
|
||
organizationLogistics: async (_: unknown, args: { organizationId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
return await prisma.logistics.findMany({
|
||
where: { organizationId: args.organizationId },
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
|
||
// Входящие заявки
|
||
incomingRequests: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
return await prisma.counterpartyRequest.findMany({
|
||
where: {
|
||
receiverId: currentUser.organization.id,
|
||
status: 'PENDING',
|
||
},
|
||
include: {
|
||
sender: {
|
||
include: {
|
||
users: true,
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
receiver: {
|
||
include: {
|
||
users: true,
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
|
||
// Исходящие заявки
|
||
outgoingRequests: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
return await prisma.counterpartyRequest.findMany({
|
||
where: {
|
||
senderId: currentUser.organization.id,
|
||
status: { in: ['PENDING', 'REJECTED'] },
|
||
},
|
||
include: {
|
||
sender: {
|
||
include: {
|
||
users: true,
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
receiver: {
|
||
include: {
|
||
users: true,
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
|
||
// Сообщения с контрагентом
|
||
messages: async (
|
||
_: unknown,
|
||
args: { counterpartyId: string; limit?: number; offset?: number },
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
const limit = args.limit || 50
|
||
const offset = args.offset || 0
|
||
|
||
const messages = await prisma.message.findMany({
|
||
where: {
|
||
OR: [
|
||
{
|
||
senderOrganizationId: currentUser.organization.id,
|
||
receiverOrganizationId: args.counterpartyId,
|
||
},
|
||
{
|
||
senderOrganizationId: args.counterpartyId,
|
||
receiverOrganizationId: currentUser.organization.id,
|
||
},
|
||
],
|
||
},
|
||
include: {
|
||
sender: true,
|
||
senderOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
receiverOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'asc' },
|
||
take: limit,
|
||
skip: offset,
|
||
})
|
||
|
||
return messages
|
||
},
|
||
|
||
// Список чатов (последние сообщения с каждым контрагентом)
|
||
conversations: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Получаем всех контрагентов
|
||
const counterparties = await prisma.counterparty.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
counterparty: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// Для каждого контрагента получаем последнее сообщение и количество непрочитанных
|
||
const conversations = await Promise.all(
|
||
counterparties.map(async (cp) => {
|
||
const counterpartyId = cp.counterparty.id
|
||
|
||
// Последнее сообщение с этим контрагентом
|
||
const lastMessage = await prisma.message.findFirst({
|
||
where: {
|
||
OR: [
|
||
{
|
||
senderOrganizationId: currentUser.organization!.id,
|
||
receiverOrganizationId: counterpartyId,
|
||
},
|
||
{
|
||
senderOrganizationId: counterpartyId,
|
||
receiverOrganizationId: currentUser.organization!.id,
|
||
},
|
||
],
|
||
},
|
||
include: {
|
||
sender: true,
|
||
senderOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
receiverOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
// Количество непрочитанных сообщений от этого контрагента
|
||
const unreadCount = await prisma.message.count({
|
||
where: {
|
||
senderOrganizationId: counterpartyId,
|
||
receiverOrganizationId: currentUser.organization!.id,
|
||
isRead: false,
|
||
},
|
||
})
|
||
|
||
// Если есть сообщения с этим контрагентом, включаем его в список
|
||
if (lastMessage) {
|
||
return {
|
||
id: `${currentUser.organization!.id}-${counterpartyId}`,
|
||
counterparty: cp.counterparty,
|
||
lastMessage,
|
||
unreadCount,
|
||
updatedAt: lastMessage.createdAt,
|
||
}
|
||
}
|
||
|
||
return null
|
||
}),
|
||
)
|
||
|
||
// Фильтруем null значения и сортируем по времени последнего сообщения
|
||
return conversations
|
||
.filter((conv) => conv !== null)
|
||
.sort((a, b) => new Date(b!.updatedAt).getTime() - new Date(a!.updatedAt).getTime())
|
||
},
|
||
|
||
// Мои услуги
|
||
myServices: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Услуги доступны только для фулфилмент центров')
|
||
}
|
||
|
||
return await prisma.service.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
|
||
// Расходники селлеров (материалы клиентов на складе фулфилмента)
|
||
mySupplies: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
return [] // Только фулфилменты имеют расходники
|
||
}
|
||
|
||
// Получаем ВСЕ расходники из таблицы supply для фулфилмента
|
||
const allSupplies = await prisma.supply.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
// Преобразуем старую структуру в новую согласно GraphQL схеме
|
||
const transformedSupplies = allSupplies.map((supply) => ({
|
||
id: supply.id,
|
||
name: supply.name,
|
||
description: supply.description,
|
||
pricePerUnit: supply.price ? parseFloat(supply.price.toString()) : null, // Конвертируем Decimal в Number
|
||
unit: supply.unit || 'шт', // Единица измерения
|
||
imageUrl: supply.imageUrl,
|
||
warehouseStock: supply.currentStock || 0, // Остаток на складе
|
||
isAvailable: (supply.currentStock || 0) > 0, // Есть ли в наличии
|
||
warehouseConsumableId: supply.id, // Связь со складом (пока используем тот же ID)
|
||
createdAt: supply.createdAt,
|
||
updatedAt: supply.updatedAt,
|
||
organization: supply.organization,
|
||
}))
|
||
|
||
console.warn('🔥 SUPPLIES RESOLVER - NEW FORMAT:', {
|
||
organizationId: currentUser.organization.id,
|
||
suppliesCount: transformedSupplies.length,
|
||
supplies: transformedSupplies.map((s) => ({
|
||
id: s.id,
|
||
name: s.name,
|
||
pricePerUnit: s.pricePerUnit,
|
||
warehouseStock: s.warehouseStock,
|
||
isAvailable: s.isAvailable,
|
||
})),
|
||
})
|
||
|
||
return transformedSupplies
|
||
},
|
||
|
||
// Доступные расходники для рецептур селлеров (только с ценой и в наличии)
|
||
getAvailableSuppliesForRecipe: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Селлеры могут получать расходники от своих фулфилмент-партнеров
|
||
if (currentUser.organization.type !== 'SELLER') {
|
||
return [] // Только селлеры используют рецептуры
|
||
}
|
||
|
||
// TODO: В будущем здесь будет логика получения расходников от партнерских фулфилментов
|
||
// Пока возвращаем пустой массив, так как эта функциональность еще разрабатывается
|
||
console.warn('🔥 getAvailableSuppliesForRecipe called for seller:', {
|
||
sellerId: currentUser.organization.id,
|
||
sellerName: currentUser.organization.name,
|
||
})
|
||
|
||
return []
|
||
},
|
||
|
||
// Расходники фулфилмента из склада (новая архитектура - синхронизация со склада)
|
||
myFulfillmentSupplies: async (_: unknown, __: unknown, context: Context) => {
|
||
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥')
|
||
|
||
if (!context.user) {
|
||
console.warn('❌ No user in context')
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
console.warn('👤 Current user:', {
|
||
id: currentUser?.id,
|
||
phone: currentUser?.phone,
|
||
organizationId: currentUser?.organizationId,
|
||
organizationType: currentUser?.organization?.type,
|
||
organizationName: currentUser?.organization?.name,
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
console.warn('❌ No organization for user')
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
console.warn('❌ User organization is not FULFILLMENT:', currentUser.organization.type)
|
||
throw new GraphQLError('Доступ только для фулфилмент центров')
|
||
}
|
||
|
||
// Получаем расходники фулфилмента из таблицы Supply
|
||
const supplies = await prisma.supply.findMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
type: 'FULFILLMENT_CONSUMABLES', // Только расходники фулфилмента
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
// Логирование для отладки
|
||
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥')
|
||
console.warn('📊 Расходники фулфилмента из склада:', {
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
suppliesCount: supplies.length,
|
||
supplies: supplies.map((s) => ({
|
||
id: s.id,
|
||
name: s.name,
|
||
type: s.type,
|
||
status: s.status,
|
||
currentStock: s.currentStock,
|
||
quantity: s.quantity,
|
||
})),
|
||
})
|
||
|
||
// Преобразуем в формат для фронтенда
|
||
return supplies.map((supply) => ({
|
||
...supply,
|
||
price: supply.price ? parseFloat(supply.price.toString()) : 0,
|
||
shippedQuantity: 0, // Добавляем для совместимости
|
||
}))
|
||
},
|
||
|
||
// Заказы поставок расходников
|
||
supplyOrders: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Возвращаем заказы где текущая организация является заказчиком, поставщиком, получателем или логистическим партнером
|
||
const orders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
OR: [
|
||
{ organizationId: currentUser.organization.id }, // Заказы созданные организацией
|
||
{ partnerId: currentUser.organization.id }, // Заказы где организация - поставщик
|
||
{ fulfillmentCenterId: currentUser.organization.id }, // Заказы где организация - получатель (фулфилмент)
|
||
{ logisticsPartnerId: currentUser.organization.id }, // Заказы где организация - логистический партнер
|
||
],
|
||
},
|
||
include: {
|
||
partner: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
fulfillmentCenter: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
return orders
|
||
},
|
||
|
||
// Счетчик поставок, требующих одобрения
|
||
pendingSuppliesCount: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Считаем заказы поставок, требующие действий
|
||
|
||
// Расходники фулфилмента (созданные нами для себя) - требуют действий по статусам
|
||
const ourSupplyOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
organizationId: currentUser.organization.id, // Создали мы
|
||
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
|
||
status: { in: ['CONFIRMED', 'IN_TRANSIT'] }, // Подтверждено или в пути
|
||
},
|
||
})
|
||
|
||
// Расходники селлеров (созданные другими для нас) - требуют действий фулфилмента
|
||
const sellerSupplyOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
|
||
organizationId: { not: currentUser.organization.id }, // Создали НЕ мы
|
||
status: {
|
||
in: [
|
||
'SUPPLIER_APPROVED', // Поставщик подтвердил - нужно назначить логистику
|
||
'IN_TRANSIT', // В пути - нужно подтвердить получение
|
||
],
|
||
},
|
||
},
|
||
})
|
||
|
||
// 🔔 ВХОДЯЩИЕ ЗАКАЗЫ ДЛЯ ПОСТАВЩИКОВ (WHOLESALE) - требуют подтверждения
|
||
const incomingSupplierOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
partnerId: currentUser.organization.id, // Мы - поставщик
|
||
status: 'PENDING', // Ожидает подтверждения от поставщика
|
||
},
|
||
})
|
||
|
||
// 🚚 ЛОГИСТИЧЕСКИЕ ЗАЯВКИ ДЛЯ ЛОГИСТИКИ (LOGIST) - требуют действий логистики
|
||
const logisticsOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
logisticsPartnerId: currentUser.organization.id, // Мы - назначенная логистика
|
||
status: {
|
||
in: [
|
||
'CONFIRMED', // Подтверждено фулфилментом - нужно подтвердить логистикой
|
||
'LOGISTICS_CONFIRMED', // Подтверждено логистикой - нужно забрать товар у поставщика
|
||
],
|
||
},
|
||
},
|
||
})
|
||
|
||
// Общий счетчик поставок в зависимости от типа организации
|
||
let pendingSupplyOrders = 0
|
||
if (currentUser.organization.type === 'FULFILLMENT') {
|
||
pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders
|
||
} else if (currentUser.organization.type === 'WHOLESALE') {
|
||
pendingSupplyOrders = incomingSupplierOrders
|
||
} else if (currentUser.organization.type === 'LOGIST') {
|
||
pendingSupplyOrders = logisticsOrders
|
||
} else if (currentUser.organization.type === 'SELLER') {
|
||
pendingSupplyOrders = 0 // Селлеры не подтверждают поставки, только отслеживают
|
||
}
|
||
|
||
// Считаем входящие заявки на партнерство со статусом PENDING
|
||
const pendingIncomingRequests = await prisma.counterpartyRequest.count({
|
||
where: {
|
||
receiverId: currentUser.organization.id,
|
||
status: 'PENDING',
|
||
},
|
||
})
|
||
|
||
return {
|
||
supplyOrders: pendingSupplyOrders,
|
||
ourSupplyOrders: ourSupplyOrders, // Расходники фулфилмента
|
||
sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров
|
||
incomingSupplierOrders: incomingSupplierOrders, // 🔔 Входящие заказы для поставщиков
|
||
logisticsOrders: logisticsOrders, // 🚚 Логистические заявки для логистики
|
||
incomingRequests: pendingIncomingRequests,
|
||
total: pendingSupplyOrders + pendingIncomingRequests,
|
||
}
|
||
},
|
||
|
||
// Статистика склада фулфилмента с изменениями за сутки
|
||
fulfillmentWarehouseStats: async (_: unknown, __: unknown, context: Context) => {
|
||
console.warn('🔥 FULFILLMENT WAREHOUSE STATS RESOLVER CALLED')
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступ разрешен только для фулфилмент-центров')
|
||
}
|
||
|
||
const organizationId = currentUser.organization.id
|
||
|
||
// Получаем дату начала суток (24 часа назад)
|
||
const oneDayAgo = new Date()
|
||
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
|
||
|
||
console.warn(`🏢 Organization ID: ${organizationId}, Date 24h ago: ${oneDayAgo.toISOString()}`)
|
||
|
||
// Сначала проверим ВСЕ заказы поставок
|
||
const allSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: { status: 'DELIVERED' },
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
organization: { select: { id: true, name: true, type: true } },
|
||
},
|
||
})
|
||
console.warn(`📦 ALL DELIVERED ORDERS: ${allSupplyOrders.length}`)
|
||
allSupplyOrders.forEach((order) => {
|
||
console.warn(
|
||
` Order ${order.id}: org=${order.organizationId} (${order.organization?.name}), fulfillment=${order.fulfillmentCenterId}, items=${order.items.length}`,
|
||
)
|
||
})
|
||
|
||
// Продукты (товары от селлеров) - заказы К нам, но исключаем расходники фулфилмента
|
||
const sellerDeliveredOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
fulfillmentCenterId: organizationId, // Доставлено к нам (фулфилменту)
|
||
organizationId: { not: organizationId }, // ИСПРАВЛЕНО: исключаем заказы самого фулфилмента
|
||
status: 'DELIVERED',
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
console.warn(`🛒 SELLER ORDERS TO FULFILLMENT: ${sellerDeliveredOrders.length}`)
|
||
|
||
const productsCount = sellerDeliveredOrders.reduce(
|
||
(sum, order) =>
|
||
sum +
|
||
order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'PRODUCT' ? item.quantity : 0), 0),
|
||
0,
|
||
)
|
||
// Изменения товаров за сутки (от селлеров)
|
||
const recentSellerDeliveredOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
fulfillmentCenterId: organizationId, // К нам
|
||
organizationId: { not: organizationId }, // От селлеров
|
||
status: 'DELIVERED',
|
||
updatedAt: { gte: oneDayAgo },
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
const productsChangeToday = recentSellerDeliveredOrders.reduce(
|
||
(sum, order) =>
|
||
sum +
|
||
order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'PRODUCT' ? item.quantity : 0), 0),
|
||
0,
|
||
)
|
||
|
||
// Товары (готовые товары = все продукты, не расходники)
|
||
const goodsCount = productsCount // Готовые товары = все продукты
|
||
const goodsChangeToday = productsChangeToday // Изменения товаров = изменения продуктов
|
||
|
||
// Брак
|
||
const defectsCount = 0 // TODO: реальные данные о браке
|
||
const defectsChangeToday = 0
|
||
|
||
// Возвраты с ПВЗ
|
||
const pvzReturnsCount = 0 // TODO: реальные данные о возвратах
|
||
const pvzReturnsChangeToday = 0
|
||
|
||
// Расходники фулфилмента - заказы ОТ фулфилмента К поставщикам, НО доставленные на склад фулфилмента
|
||
// Согласно правилам: фулфилмент заказывает расходники у поставщиков для своих операционных нужд
|
||
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Заказчик = фулфилмент
|
||
fulfillmentCenterId: organizationId, // ИСПРАВЛЕНО: доставлено НА склад фулфилмента
|
||
status: 'DELIVERED',
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
console.warn(`🏭 FULFILLMENT SUPPLY ORDERS: ${fulfillmentSupplyOrders.length}`)
|
||
|
||
// Подсчитываем количество из таблицы Supply (актуальные остатки на складе фулфилмента)
|
||
// ИСПРАВЛЕНО: считаем только расходники фулфилмента, исключаем расходники селлеров
|
||
const fulfillmentSuppliesFromWarehouse = await prisma.supply.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Склад фулфилмента
|
||
type: 'FULFILLMENT_CONSUMABLES', // ТОЛЬКО расходники фулфилмента
|
||
},
|
||
})
|
||
|
||
const fulfillmentSuppliesCount = fulfillmentSuppliesFromWarehouse.reduce(
|
||
(sum, supply) => sum + (supply.currentStock || 0),
|
||
0,
|
||
)
|
||
|
||
console.warn(
|
||
`🔥 FULFILLMENT SUPPLIES DEBUG: organizationId=${organizationId}, ordersCount=${fulfillmentSupplyOrders.length}, warehouseCount=${fulfillmentSuppliesFromWarehouse.length}, totalStock=${fulfillmentSuppliesCount}`,
|
||
)
|
||
console.warn(
|
||
'📦 FULFILLMENT SUPPLIES BREAKDOWN:',
|
||
fulfillmentSuppliesFromWarehouse.map((supply) => ({
|
||
name: supply.name,
|
||
currentStock: supply.currentStock,
|
||
supplier: supply.supplier,
|
||
})),
|
||
)
|
||
|
||
// Изменения расходников фулфилмента за сутки (ПРИБЫЛО)
|
||
// Ищем заказы фулфилмента, доставленные на его склад за последние сутки
|
||
const fulfillmentSuppliesReceivedToday = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Заказчик = фулфилмент
|
||
fulfillmentCenterId: organizationId, // ИСПРАВЛЕНО: доставлено НА склад фулфилмента
|
||
status: 'DELIVERED',
|
||
updatedAt: { gte: oneDayAgo },
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
const fulfillmentSuppliesChangeToday = fulfillmentSuppliesReceivedToday.reduce(
|
||
(sum, order) =>
|
||
sum +
|
||
order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'CONSUMABLE' ? item.quantity : 0), 0),
|
||
0,
|
||
)
|
||
|
||
console.warn(
|
||
`📊 FULFILLMENT SUPPLIES RECEIVED TODAY (ПРИБЫЛО): ${fulfillmentSuppliesReceivedToday.length} orders, ${fulfillmentSuppliesChangeToday} items`,
|
||
)
|
||
|
||
// Расходники селлеров - получаем из таблицы Supply (актуальные остатки на складе фулфилмента)
|
||
// ИСПРАВЛЕНО: считаем из Supply с типом SELLER_CONSUMABLES
|
||
const sellerSuppliesFromWarehouse = await prisma.supply.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Склад фулфилмента
|
||
type: 'SELLER_CONSUMABLES', // ТОЛЬКО расходники селлеров
|
||
},
|
||
})
|
||
|
||
const sellerSuppliesCount = sellerSuppliesFromWarehouse.reduce(
|
||
(sum, supply) => sum + (supply.currentStock || 0),
|
||
0,
|
||
)
|
||
|
||
console.warn(`💼 SELLER SUPPLIES DEBUG: totalCount=${sellerSuppliesCount} (from Supply warehouse)`)
|
||
|
||
// Изменения расходников селлеров за сутки - считаем из Supply записей, созданных за сутки
|
||
const sellerSuppliesReceivedToday = await prisma.supply.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Склад фулфилмента
|
||
type: 'SELLER_CONSUMABLES', // ТОЛЬКО расходники селлеров
|
||
createdAt: { gte: oneDayAgo }, // Созданы за последние сутки
|
||
},
|
||
})
|
||
|
||
const sellerSuppliesChangeToday = sellerSuppliesReceivedToday.reduce(
|
||
(sum, supply) => sum + (supply.currentStock || 0),
|
||
0,
|
||
)
|
||
|
||
console.warn(
|
||
`📊 SELLER SUPPLIES RECEIVED TODAY: ${sellerSuppliesReceivedToday.length} supplies, ${sellerSuppliesChangeToday} items`,
|
||
)
|
||
|
||
// Вычисляем процентные изменения
|
||
const calculatePercentChange = (current: number, change: number): number => {
|
||
if (current === 0) return change > 0 ? 100 : 0
|
||
return (change / current) * 100
|
||
}
|
||
|
||
const result = {
|
||
products: {
|
||
current: productsCount,
|
||
change: productsChangeToday,
|
||
percentChange: calculatePercentChange(productsCount, productsChangeToday),
|
||
},
|
||
goods: {
|
||
current: goodsCount,
|
||
change: goodsChangeToday,
|
||
percentChange: calculatePercentChange(goodsCount, goodsChangeToday),
|
||
},
|
||
defects: {
|
||
current: defectsCount,
|
||
change: defectsChangeToday,
|
||
percentChange: calculatePercentChange(defectsCount, defectsChangeToday),
|
||
},
|
||
pvzReturns: {
|
||
current: pvzReturnsCount,
|
||
change: pvzReturnsChangeToday,
|
||
percentChange: calculatePercentChange(pvzReturnsCount, pvzReturnsChangeToday),
|
||
},
|
||
fulfillmentSupplies: {
|
||
current: fulfillmentSuppliesCount,
|
||
change: fulfillmentSuppliesChangeToday,
|
||
percentChange: calculatePercentChange(fulfillmentSuppliesCount, fulfillmentSuppliesChangeToday),
|
||
},
|
||
sellerSupplies: {
|
||
current: sellerSuppliesCount,
|
||
change: sellerSuppliesChangeToday,
|
||
percentChange: calculatePercentChange(sellerSuppliesCount, sellerSuppliesChangeToday),
|
||
},
|
||
}
|
||
|
||
console.warn('🏁 FINAL WAREHOUSE STATS RESULT:', JSON.stringify(result, null, 2))
|
||
|
||
return result
|
||
},
|
||
|
||
// Логистика организации
|
||
myLogistics: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
return await prisma.logistics.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
|
||
// Логистические партнеры (организации-логисты)
|
||
logisticsPartners: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
// Получаем все организации типа LOGIST
|
||
return await prisma.organization.findMany({
|
||
where: {
|
||
type: 'LOGIST',
|
||
// Убираем фильтр по статусу пока не определим правильные значения
|
||
},
|
||
orderBy: { createdAt: 'desc' }, // Сортируем по дате создания вместо name
|
||
})
|
||
},
|
||
|
||
// Мои поставки Wildberries
|
||
myWildberriesSupplies: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
return await prisma.wildberriesSupply.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
organization: true,
|
||
cards: true,
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
|
||
// Расходники селлеров на складе фулфилмента (новый resolver)
|
||
sellerSuppliesOnWarehouse: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Только фулфилмент может получать расходники селлеров на своем складе
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступ разрешен только для фулфилмент-центров')
|
||
}
|
||
|
||
// ИСПРАВЛЕНО: Усиленная фильтрация расходников селлеров
|
||
const sellerSupplies = await prisma.supply.findMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id, // На складе этого фулфилмента
|
||
type: 'SELLER_CONSUMABLES' as const, // Только расходники селлеров
|
||
sellerOwnerId: { not: null }, // ОБЯЗАТЕЛЬНО должен быть владелец-селлер
|
||
},
|
||
include: {
|
||
organization: true, // Фулфилмент-центр (хранитель)
|
||
sellerOwner: true, // Селлер-владелец расходников
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
// Логирование для отладки
|
||
console.warn('🔍 ИСПРАВЛЕНО: Запрос расходников селлеров на складе фулфилмента:', {
|
||
fulfillmentId: currentUser.organization.id,
|
||
fulfillmentName: currentUser.organization.name,
|
||
totalSupplies: sellerSupplies.length,
|
||
sellerSupplies: sellerSupplies.map((supply) => ({
|
||
id: supply.id,
|
||
name: supply.name,
|
||
type: supply.type,
|
||
sellerOwnerId: supply.sellerOwnerId,
|
||
sellerOwnerName: supply.sellerOwner?.name || supply.sellerOwner?.fullName,
|
||
currentStock: supply.currentStock,
|
||
})),
|
||
})
|
||
|
||
// ДВОЙНАЯ ПРОВЕРКА: Фильтруем на уровне кода для гарантии
|
||
const filteredSupplies = sellerSupplies.filter((supply) => {
|
||
const isValid =
|
||
supply.type === 'SELLER_CONSUMABLES' && supply.sellerOwnerId != null && supply.sellerOwner != null
|
||
|
||
if (!isValid) {
|
||
console.warn('⚠️ ОТФИЛЬТРОВАН некорректный расходник:', {
|
||
id: supply.id,
|
||
name: supply.name,
|
||
type: supply.type,
|
||
sellerOwnerId: supply.sellerOwnerId,
|
||
hasSellerOwner: !!supply.sellerOwner,
|
||
})
|
||
}
|
||
|
||
return isValid
|
||
})
|
||
|
||
console.warn('✅ ФИНАЛЬНЫЙ РЕЗУЛЬТАТ после фильтрации:', {
|
||
originalCount: sellerSupplies.length,
|
||
filteredCount: filteredSupplies.length,
|
||
removedCount: sellerSupplies.length - filteredSupplies.length,
|
||
})
|
||
|
||
return filteredSupplies
|
||
},
|
||
|
||
// Мои товары и расходники (для поставщиков)
|
||
myProducts: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это поставщик
|
||
if (currentUser.organization.type !== 'WHOLESALE') {
|
||
throw new GraphQLError('Товары доступны только для поставщиков')
|
||
}
|
||
|
||
const products = await prisma.product.findMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
// Показываем и товары, и расходники поставщика
|
||
},
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
console.warn('🔥 MY_PRODUCTS RESOLVER DEBUG:', {
|
||
userId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
organizationName: currentUser.organization.name,
|
||
totalProducts: products.length,
|
||
productTypes: products.map((p) => ({
|
||
id: p.id,
|
||
name: p.name,
|
||
article: p.article,
|
||
type: p.type,
|
||
isActive: p.isActive,
|
||
createdAt: p.createdAt,
|
||
})),
|
||
})
|
||
|
||
return products
|
||
},
|
||
|
||
// Товары на складе фулфилмента (из доставленных заказов поставок)
|
||
warehouseProducts: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Товары склада доступны только для фулфилмент центров')
|
||
}
|
||
|
||
// Получаем все доставленные заказы поставок, где этот фулфилмент центр является получателем
|
||
const deliveredSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
fulfillmentCenterId: currentUser.organization.id,
|
||
status: 'DELIVERED', // Только доставленные заказы
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true, // Включаем информацию о поставщике
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true, // Селлер, который сделал заказ
|
||
partner: true, // Поставщик товаров
|
||
},
|
||
})
|
||
|
||
// Собираем все товары из доставленных заказов
|
||
const allProducts: unknown[] = []
|
||
|
||
console.warn('🔍 Резолвер warehouseProducts (доставленные заказы):', {
|
||
currentUserId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
deliveredOrdersCount: deliveredSupplyOrders.length,
|
||
orders: deliveredSupplyOrders.map((order) => ({
|
||
id: order.id,
|
||
sellerName: order.organization.name || order.organization.fullName,
|
||
supplierName: order.partner.name || order.partner.fullName,
|
||
status: order.status,
|
||
itemsCount: order.items.length,
|
||
deliveryDate: order.deliveryDate,
|
||
})),
|
||
})
|
||
|
||
for (const order of deliveredSupplyOrders) {
|
||
console.warn(
|
||
`📦 Заказ от селлера ${order.organization.name} у поставщика ${order.partner.name}:`,
|
||
order.items.map((item) => ({
|
||
productId: item.product.id,
|
||
productName: item.product.name,
|
||
article: item.product.article,
|
||
orderedQuantity: item.quantity,
|
||
price: item.price,
|
||
})),
|
||
)
|
||
|
||
for (const item of order.items) {
|
||
// Добавляем только товары типа PRODUCT, исключаем расходники
|
||
if (item.product.type === 'PRODUCT') {
|
||
allProducts.push({
|
||
...item.product,
|
||
// Дополнительная информация о заказе
|
||
orderedQuantity: item.quantity,
|
||
orderedPrice: item.price,
|
||
orderId: order.id,
|
||
orderDate: order.deliveryDate,
|
||
seller: order.organization, // Селлер, который заказал
|
||
supplier: order.partner, // Поставщик товара
|
||
// Для совместимости с существующим интерфейсом
|
||
organization: order.organization, // Указываем селлера как владельца
|
||
})
|
||
} else {
|
||
console.warn('🚫 Исключен расходник из основного склада фулфилмента:', {
|
||
name: item.product.name,
|
||
type: item.product.type,
|
||
orderId: order.id,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
console.warn('✅ Итого товаров на складе фулфилмента (из доставленных заказов):', allProducts.length)
|
||
return allProducts
|
||
},
|
||
|
||
// Все товары и расходники поставщиков для маркета
|
||
allProducts: async (_: unknown, args: { search?: string; category?: string }, context: Context) => {
|
||
console.warn('🛍️ ALL_PRODUCTS RESOLVER - ВЫЗВАН:', {
|
||
userId: context.user?.id,
|
||
search: args.search,
|
||
category: args.category,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const where: Record<string, unknown> = {
|
||
isActive: true, // Показываем только активные товары
|
||
// Показываем и товары, и расходники поставщиков
|
||
organization: {
|
||
type: 'WHOLESALE', // Только товары поставщиков
|
||
},
|
||
}
|
||
|
||
if (args.search) {
|
||
where.OR = [
|
||
{ name: { contains: args.search, mode: 'insensitive' } },
|
||
{ article: { contains: args.search, mode: 'insensitive' } },
|
||
{ description: { contains: args.search, mode: 'insensitive' } },
|
||
{ brand: { contains: args.search, mode: 'insensitive' } },
|
||
]
|
||
}
|
||
|
||
if (args.category) {
|
||
where.categoryId = args.category
|
||
}
|
||
|
||
const products = await prisma.product.findMany({
|
||
where,
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
take: 100, // Ограничиваем количество результатов
|
||
})
|
||
|
||
console.warn('🔥 ALL_PRODUCTS RESOLVER DEBUG:', {
|
||
searchArgs: args,
|
||
whereCondition: where,
|
||
totalProducts: products.length,
|
||
productTypes: products.map((p) => ({
|
||
id: p.id,
|
||
name: p.name,
|
||
type: p.type,
|
||
org: p.organization.name,
|
||
})),
|
||
})
|
||
|
||
return products
|
||
},
|
||
|
||
// Товары конкретной организации (для формы создания поставки)
|
||
organizationProducts: async (
|
||
_: unknown,
|
||
args: { organizationId: string; search?: string; category?: string; type?: string },
|
||
context: Context,
|
||
) => {
|
||
console.warn('🏢 ORGANIZATION_PRODUCTS RESOLVER - ВЫЗВАН:', {
|
||
userId: context.user?.id,
|
||
organizationId: args.organizationId,
|
||
search: args.search,
|
||
category: args.category,
|
||
type: args.type,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const where: Record<string, unknown> = {
|
||
isActive: true, // Показываем только активные товары
|
||
organizationId: args.organizationId, // Фильтруем по конкретной организации
|
||
type: args.type || 'ТОВАР', // Показываем только товары по умолчанию, НЕ расходники согласно development-checklist.md
|
||
}
|
||
|
||
if (args.search) {
|
||
where.OR = [
|
||
{ name: { contains: args.search, mode: 'insensitive' } },
|
||
{ article: { contains: args.search, mode: 'insensitive' } },
|
||
{ description: { contains: args.search, mode: 'insensitive' } },
|
||
{ brand: { contains: args.search, mode: 'insensitive' } },
|
||
]
|
||
}
|
||
|
||
if (args.category) {
|
||
where.categoryId = args.category
|
||
}
|
||
|
||
const products = await prisma.product.findMany({
|
||
where,
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
take: 100, // Ограничиваем количество результатов
|
||
})
|
||
|
||
console.warn('🔥 ORGANIZATION_PRODUCTS RESOLVER DEBUG:', {
|
||
organizationId: args.organizationId,
|
||
searchArgs: args,
|
||
whereCondition: where,
|
||
totalProducts: products.length,
|
||
productTypes: products.map((p) => ({
|
||
id: p.id,
|
||
name: p.name,
|
||
type: p.type,
|
||
isActive: p.isActive,
|
||
})),
|
||
})
|
||
|
||
return products
|
||
},
|
||
|
||
// Все категории
|
||
categories: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user && !context.admin) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
return await prisma.category.findMany({
|
||
orderBy: { name: 'asc' },
|
||
})
|
||
},
|
||
|
||
// Публичные услуги контрагента (для фулфилмента)
|
||
counterpartyServices: async (_: unknown, args: { organizationId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что запрашиваемая организация является контрагентом
|
||
const counterparty = await prisma.counterparty.findFirst({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterpartyId: args.organizationId,
|
||
},
|
||
})
|
||
|
||
if (!counterparty) {
|
||
throw new GraphQLError('Организация не является вашим контрагентом')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
const targetOrganization = await prisma.organization.findUnique({
|
||
where: { id: args.organizationId },
|
||
})
|
||
|
||
if (!targetOrganization || targetOrganization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Услуги доступны только у фулфилмент центров')
|
||
}
|
||
|
||
return await prisma.service.findMany({
|
||
where: { organizationId: args.organizationId },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
|
||
// Публичные расходники контрагента (для поставщиков)
|
||
counterpartySupplies: async (_: unknown, args: { organizationId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что запрашиваемая организация является контрагентом
|
||
const counterparty = await prisma.counterparty.findFirst({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterpartyId: args.organizationId,
|
||
},
|
||
})
|
||
|
||
if (!counterparty) {
|
||
throw new GraphQLError('Организация не является вашим контрагентом')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр (у них есть расходники)
|
||
const targetOrganization = await prisma.organization.findUnique({
|
||
where: { id: args.organizationId },
|
||
})
|
||
|
||
if (!targetOrganization || targetOrganization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Расходники доступны только у фулфилмент центров')
|
||
}
|
||
|
||
return await prisma.supply.findMany({
|
||
where: { organizationId: args.organizationId },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
|
||
// Корзина пользователя
|
||
myCart: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Найти или создать корзину для организации
|
||
let cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
if (!cart) {
|
||
cart = await prisma.cart.create({
|
||
data: {
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
})
|
||
}
|
||
|
||
return cart
|
||
},
|
||
|
||
// Избранные товары пользователя
|
||
myFavorites: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Получаем избранные товары
|
||
const favorites = await prisma.favorites.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
return favorites.map((favorite) => favorite.product)
|
||
},
|
||
|
||
// Сотрудники организации
|
||
myEmployees: async (_: unknown, __: unknown, context: Context) => {
|
||
console.warn('🔍 myEmployees resolver called')
|
||
|
||
if (!context.user) {
|
||
console.warn('❌ No user in context for myEmployees')
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
console.warn('✅ User authenticated for myEmployees:', context.user.id)
|
||
|
||
try {
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
console.warn('❌ User has no organization')
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
console.warn('📊 User organization type:', currentUser.organization.type)
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
console.warn('❌ Not a fulfillment center')
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
const employees = await prisma.employee.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
organization: true,
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
console.warn('👥 Found employees:', employees.length)
|
||
return employees
|
||
} catch (error) {
|
||
console.error('❌ Error in myEmployees resolver:', error)
|
||
throw error
|
||
}
|
||
},
|
||
|
||
// Получение сотрудника по ID
|
||
employee: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
const employee = await prisma.employee.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return employee
|
||
},
|
||
|
||
// Получить табель сотрудника за месяц
|
||
employeeSchedule: async (
|
||
_: unknown,
|
||
args: { employeeId: string; year: number; month: number },
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
// Проверяем что сотрудник принадлежит организации
|
||
const employee = await prisma.employee.findFirst({
|
||
where: {
|
||
id: args.employeeId,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!employee) {
|
||
throw new GraphQLError('Сотрудник не найден')
|
||
}
|
||
|
||
// Получаем записи табеля за указанный месяц
|
||
const startDate = new Date(args.year, args.month, 1)
|
||
const endDate = new Date(args.year, args.month + 1, 0)
|
||
|
||
const scheduleRecords = await prisma.employeeSchedule.findMany({
|
||
where: {
|
||
employeeId: args.employeeId,
|
||
date: {
|
||
gte: startDate,
|
||
lte: endDate,
|
||
},
|
||
},
|
||
orderBy: {
|
||
date: 'asc',
|
||
},
|
||
})
|
||
|
||
return scheduleRecords
|
||
},
|
||
},
|
||
|
||
Mutation: {
|
||
sendSmsCode: async (_: unknown, args: { phone: string }) => {
|
||
const result = await smsService.sendSmsCode(args.phone)
|
||
return {
|
||
success: result.success,
|
||
message: result.message || 'SMS код отправлен',
|
||
}
|
||
},
|
||
|
||
verifySmsCode: async (_: unknown, args: { phone: string; code: string }) => {
|
||
const verificationResult = await smsService.verifySmsCode(args.phone, args.code)
|
||
|
||
if (!verificationResult.success) {
|
||
return {
|
||
success: false,
|
||
message: verificationResult.message || 'Неверный код',
|
||
}
|
||
}
|
||
|
||
// Найти или создать пользователя
|
||
const formattedPhone = args.phone.replace(/\D/g, '')
|
||
let user = await prisma.user.findUnique({
|
||
where: { phone: formattedPhone },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user) {
|
||
user = await prisma.user.create({
|
||
data: {
|
||
phone: formattedPhone,
|
||
},
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
}
|
||
|
||
const token = generateToken({
|
||
userId: user.id,
|
||
phone: user.phone,
|
||
})
|
||
|
||
console.warn('verifySmsCode - Generated token:', token ? `${token.substring(0, 20)}...` : 'No token')
|
||
console.warn('verifySmsCode - Full token:', token)
|
||
console.warn('verifySmsCode - User object:', {
|
||
id: user.id,
|
||
phone: user.phone,
|
||
})
|
||
|
||
const result = {
|
||
success: true,
|
||
message: 'Авторизация успешна',
|
||
token,
|
||
user,
|
||
}
|
||
|
||
console.warn('verifySmsCode - Returning result:', {
|
||
success: result.success,
|
||
hasToken: !!result.token,
|
||
hasUser: !!result.user,
|
||
message: result.message,
|
||
tokenPreview: result.token ? `${result.token.substring(0, 20)}...` : 'No token in result',
|
||
})
|
||
|
||
return result
|
||
},
|
||
|
||
verifyInn: async (_: unknown, args: { inn: string }) => {
|
||
// Валидируем ИНН
|
||
if (!dadataService.validateInn(args.inn)) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверный формат ИНН',
|
||
}
|
||
}
|
||
|
||
// Получаем данные организации из DaData
|
||
const organizationData = await dadataService.getOrganizationByInn(args.inn)
|
||
if (!organizationData) {
|
||
return {
|
||
success: false,
|
||
message: 'Организация с указанным ИНН не найдена',
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'ИНН найден',
|
||
organization: {
|
||
name: organizationData.name,
|
||
fullName: organizationData.fullName,
|
||
address: organizationData.address,
|
||
isActive: organizationData.isActive,
|
||
},
|
||
}
|
||
},
|
||
|
||
registerFulfillmentOrganization: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
phone: string
|
||
inn: string
|
||
type: 'FULFILLMENT' | 'LOGIST' | 'WHOLESALE'
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const { inn, type } = 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: type,
|
||
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
|
||
|
||
console.warn(`🔍 Validating ${marketplace} API key:`, {
|
||
keyLength: apiKey.length,
|
||
keyPreview: apiKey.substring(0, 20) + '...',
|
||
validateOnly,
|
||
})
|
||
|
||
// Валидируем API ключ
|
||
const validationResult = await marketplaceService.validateApiKey(marketplace, apiKey, clientId)
|
||
|
||
console.warn(`✅ Validation result for ${marketplace}:`, validationResult)
|
||
|
||
if (!validationResult.isValid) {
|
||
console.warn(`❌ Validation failed for ${marketplace}:`, validationResult.message)
|
||
return {
|
||
success: false,
|
||
message: validationResult.message,
|
||
}
|
||
}
|
||
|
||
// Если это только валидация, возвращаем результат без сохранения
|
||
if (validateOnly) {
|
||
return {
|
||
success: true,
|
||
message: 'API ключ действителен',
|
||
apiKey: {
|
||
id: 'validate-only',
|
||
marketplace,
|
||
apiKey: '***', // Скрываем реальный ключ при валидации
|
||
isActive: true,
|
||
validationData: validationResult,
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
},
|
||
}
|
||
}
|
||
|
||
// Для сохранения API ключа нужна авторизация
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация для сохранения API ключа', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
return {
|
||
success: false,
|
||
message: 'Пользователь не привязан к организации',
|
||
}
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что такого ключа еще нет
|
||
const existingKey = await prisma.apiKey.findUnique({
|
||
where: {
|
||
organizationId_marketplace: {
|
||
organizationId: user.organization.id,
|
||
marketplace,
|
||
},
|
||
},
|
||
})
|
||
|
||
if (existingKey) {
|
||
// Обновляем существующий ключ
|
||
const updatedKey = await prisma.apiKey.update({
|
||
where: { id: existingKey.id },
|
||
data: {
|
||
apiKey,
|
||
validationData: JSON.parse(JSON.stringify(validationResult.data)),
|
||
isActive: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'API ключ успешно обновлен',
|
||
apiKey: updatedKey,
|
||
}
|
||
} else {
|
||
// Создаем новый ключ
|
||
const newKey = await prisma.apiKey.create({
|
||
data: {
|
||
marketplace,
|
||
apiKey,
|
||
organizationId: user.organization.id,
|
||
validationData: JSON.parse(JSON.stringify(validationResult.data)),
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'API ключ успешно добавлен',
|
||
apiKey: newKey,
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding marketplace API key:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при добавлении API ключа',
|
||
}
|
||
}
|
||
},
|
||
|
||
removeMarketplaceApiKey: async (_: unknown, args: { marketplace: 'WILDBERRIES' | 'OZON' }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Пользователь не привязан к организации')
|
||
}
|
||
|
||
try {
|
||
await prisma.apiKey.delete({
|
||
where: {
|
||
organizationId_marketplace: {
|
||
organizationId: user.organization.id,
|
||
marketplace: args.marketplace,
|
||
},
|
||
},
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error removing marketplace API key:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
updateUserProfile: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
avatar?: string
|
||
orgPhone?: string
|
||
managerName?: string
|
||
telegram?: string
|
||
whatsapp?: string
|
||
email?: string
|
||
bankName?: string
|
||
bik?: string
|
||
accountNumber?: string
|
||
corrAccount?: string
|
||
market?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Пользователь не привязан к организации')
|
||
}
|
||
|
||
try {
|
||
const { input } = args
|
||
|
||
// Обновляем данные пользователя (аватар, имя управляющего)
|
||
const userUpdateData: { avatar?: string; managerName?: string } = {}
|
||
if (input.avatar) {
|
||
userUpdateData.avatar = input.avatar
|
||
}
|
||
if (input.managerName) {
|
||
userUpdateData.managerName = input.managerName
|
||
}
|
||
|
||
if (Object.keys(userUpdateData).length > 0) {
|
||
await prisma.user.update({
|
||
where: { id: context.user.id },
|
||
data: userUpdateData,
|
||
})
|
||
}
|
||
|
||
// Подготавливаем данные для обновления организации
|
||
const updateData: {
|
||
phones?: object
|
||
emails?: object
|
||
managementName?: string
|
||
managementPost?: string
|
||
market?: string
|
||
} = {}
|
||
|
||
// Название организации больше не обновляется через профиль
|
||
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
|
||
|
||
// Обновляем контактные данные в JSON поле phones
|
||
if (input.orgPhone) {
|
||
updateData.phones = [{ value: input.orgPhone, type: 'main' }]
|
||
}
|
||
|
||
// Обновляем email в JSON поле emails
|
||
if (input.email) {
|
||
updateData.emails = [{ value: input.email, type: 'main' }]
|
||
}
|
||
|
||
// Обновляем рынок для поставщиков
|
||
if (input.market !== undefined) {
|
||
updateData.market = input.market === 'none' ? null : input.market
|
||
}
|
||
|
||
// Сохраняем дополнительные контакты в custom полях
|
||
// Пока добавим их как дополнительные JSON поля
|
||
const customContacts: {
|
||
managerName?: string
|
||
telegram?: string
|
||
whatsapp?: string
|
||
bankDetails?: {
|
||
bankName?: string
|
||
bik?: string
|
||
accountNumber?: string
|
||
corrAccount?: string
|
||
}
|
||
} = {}
|
||
|
||
// managerName теперь сохраняется в поле пользователя, а не в JSON
|
||
|
||
if (input.telegram) {
|
||
customContacts.telegram = input.telegram
|
||
}
|
||
|
||
if (input.whatsapp) {
|
||
customContacts.whatsapp = input.whatsapp
|
||
}
|
||
|
||
if (input.bankName || input.bik || input.accountNumber || input.corrAccount) {
|
||
customContacts.bankDetails = {
|
||
bankName: input.bankName,
|
||
bik: input.bik,
|
||
accountNumber: input.accountNumber,
|
||
corrAccount: input.corrAccount,
|
||
}
|
||
}
|
||
|
||
// Если есть дополнительные контакты, сохраним их в поле managementPost временно
|
||
// В идеале нужно добавить отдельную таблицу для контактов
|
||
if (Object.keys(customContacts).length > 0) {
|
||
updateData.managementPost = JSON.stringify(customContacts)
|
||
}
|
||
|
||
// Обновляем организацию
|
||
await prisma.organization.update({
|
||
where: { id: user.organization.id },
|
||
data: updateData,
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
})
|
||
|
||
// Получаем обновленного пользователя
|
||
const updatedUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Профиль успешно обновлен',
|
||
user: updatedUser,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating user profile:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении профиля',
|
||
}
|
||
}
|
||
},
|
||
|
||
updateOrganizationByInn: async (_: unknown, args: { inn: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Пользователь не привязан к организации')
|
||
}
|
||
|
||
try {
|
||
// Валидируем ИНН
|
||
if (!dadataService.validateInn(args.inn)) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверный формат ИНН',
|
||
}
|
||
}
|
||
|
||
// Получаем данные организации из DaData
|
||
const organizationData = await dadataService.getOrganizationByInn(args.inn)
|
||
if (!organizationData) {
|
||
return {
|
||
success: false,
|
||
message: 'Организация с указанным ИНН не найдена в федеральном реестре',
|
||
}
|
||
}
|
||
|
||
// Проверяем, есть ли уже организация с таким ИНН в базе (кроме текущей)
|
||
const existingOrganization = await prisma.organization.findUnique({
|
||
where: { inn: organizationData.inn },
|
||
})
|
||
|
||
if (existingOrganization && existingOrganization.id !== user.organization.id) {
|
||
return {
|
||
success: false,
|
||
message: `Организация с ИНН ${organizationData.inn} уже существует в системе`,
|
||
}
|
||
}
|
||
|
||
// Подготавливаем данные для обновления
|
||
const updateData: Prisma.OrganizationUpdateInput = {
|
||
kpp: organizationData.kpp,
|
||
// Для селлеров не обновляем название организации (это название магазина)
|
||
...(user.organization.type !== 'SELLER' && {
|
||
name: organizationData.name,
|
||
}),
|
||
fullName: organizationData.fullName,
|
||
address: organizationData.address,
|
||
addressFull: organizationData.addressFull,
|
||
ogrn: organizationData.ogrn,
|
||
ogrnDate: organizationData.ogrnDate ? organizationData.ogrnDate.toISOString() : null,
|
||
registrationDate: organizationData.registrationDate ? organizationData.registrationDate.toISOString() : null,
|
||
liquidationDate: organizationData.liquidationDate ? organizationData.liquidationDate.toISOString() : null,
|
||
managementName: organizationData.managementName, // Всегда перезаписываем данными из DaData (может быть null)
|
||
managementPost: user.organization.managementPost, // Сохраняем кастомные данные пользователя
|
||
opfCode: organizationData.opfCode,
|
||
opfFull: organizationData.opfFull,
|
||
opfShort: organizationData.opfShort,
|
||
okato: organizationData.okato,
|
||
oktmo: organizationData.oktmo,
|
||
okpo: organizationData.okpo,
|
||
okved: organizationData.okved,
|
||
status: organizationData.status,
|
||
}
|
||
|
||
// Добавляем ИНН только если он отличается от текущего
|
||
if (user.organization.inn !== organizationData.inn) {
|
||
updateData.inn = organizationData.inn
|
||
}
|
||
|
||
// Обновляем организацию
|
||
await prisma.organization.update({
|
||
where: { id: user.organization.id },
|
||
data: updateData,
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
})
|
||
|
||
// Получаем обновленного пользователя
|
||
const updatedUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Данные организации успешно обновлены',
|
||
user: updatedUser,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating organization by INN:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении данных организации',
|
||
}
|
||
}
|
||
},
|
||
|
||
logout: () => {
|
||
// В stateless JWT системе logout происходит на клиенте
|
||
// Можно добавить blacklist токенов, если нужно
|
||
return true
|
||
},
|
||
|
||
// Отправить заявку на добавление в контрагенты
|
||
sendCounterpartyRequest: async (
|
||
_: unknown,
|
||
args: { organizationId: string; message?: string },
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.id === args.organizationId) {
|
||
throw new GraphQLError('Нельзя отправить заявку самому себе')
|
||
}
|
||
|
||
// Проверяем, что организация-получатель существует
|
||
const receiverOrganization = await prisma.organization.findUnique({
|
||
where: { id: args.organizationId },
|
||
})
|
||
|
||
if (!receiverOrganization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
try {
|
||
// Создаем или обновляем заявку
|
||
const request = await prisma.counterpartyRequest.upsert({
|
||
where: {
|
||
senderId_receiverId: {
|
||
senderId: currentUser.organization.id,
|
||
receiverId: args.organizationId,
|
||
},
|
||
},
|
||
update: {
|
||
status: 'PENDING',
|
||
message: args.message,
|
||
updatedAt: new Date(),
|
||
},
|
||
create: {
|
||
senderId: currentUser.organization.id,
|
||
receiverId: args.organizationId,
|
||
message: args.message,
|
||
status: 'PENDING',
|
||
},
|
||
include: {
|
||
sender: true,
|
||
receiver: true,
|
||
},
|
||
})
|
||
|
||
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' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// conversationId имеет формат "currentOrgId-counterpartyId"
|
||
const [, counterpartyId] = args.conversationId.split('-')
|
||
|
||
if (!counterpartyId) {
|
||
throw new GraphQLError('Неверный ID беседы')
|
||
}
|
||
|
||
// Помечаем все непрочитанные сообщения от контрагента как прочитанные
|
||
await prisma.message.updateMany({
|
||
where: {
|
||
senderOrganizationId: counterpartyId,
|
||
receiverOrganizationId: currentUser.organization.id,
|
||
isRead: false,
|
||
},
|
||
data: {
|
||
isRead: true,
|
||
},
|
||
})
|
||
|
||
return true
|
||
},
|
||
|
||
// Создать услугу
|
||
createService: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
name: string
|
||
description?: string
|
||
price: number
|
||
imageUrl?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Услуги доступны только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
const service = await prisma.service.create({
|
||
data: {
|
||
name: args.input.name,
|
||
description: args.input.description,
|
||
price: args.input.price,
|
||
imageUrl: args.input.imageUrl,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: { organization: true },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Услуга успешно создана',
|
||
service,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating service:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании услуги',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить услугу
|
||
updateService: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
input: {
|
||
name: string
|
||
description?: string
|
||
price: number
|
||
imageUrl?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что услуга принадлежит текущей организации
|
||
const existingService = await prisma.service.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingService) {
|
||
throw new GraphQLError('Услуга не найдена или нет доступа')
|
||
}
|
||
|
||
try {
|
||
const service = await prisma.service.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
description: args.input.description,
|
||
price: args.input.price,
|
||
imageUrl: args.input.imageUrl,
|
||
},
|
||
include: { organization: true },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Услуга успешно обновлена',
|
||
service,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating service:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении услуги',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить услугу
|
||
deleteService: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что услуга принадлежит текущей организации
|
||
const existingService = await prisma.service.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingService) {
|
||
throw new GraphQLError('Услуга не найдена или нет доступа')
|
||
}
|
||
|
||
try {
|
||
await prisma.service.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error deleting service:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Обновить цену расходника (новая архитектура - только цену можно редактировать)
|
||
updateSupplyPrice: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
input: {
|
||
pricePerUnit?: number | null
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Обновление цен расходников доступно только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
// Находим и обновляем расходник
|
||
const existingSupply = await prisma.supply.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingSupply) {
|
||
throw new GraphQLError('Расходник не найден')
|
||
}
|
||
|
||
const updatedSupply = await prisma.supply.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
pricePerUnit: args.input.pricePerUnit, // Обновляем цену продажи, НЕ цену закупки
|
||
updatedAt: new Date(),
|
||
},
|
||
include: { organization: true },
|
||
})
|
||
|
||
// Преобразуем в новый формат для GraphQL
|
||
const transformedSupply = {
|
||
id: updatedSupply.id,
|
||
name: updatedSupply.name,
|
||
description: updatedSupply.description,
|
||
pricePerUnit: updatedSupply.price ? parseFloat(updatedSupply.price.toString()) : null, // Конвертируем Decimal в Number
|
||
unit: updatedSupply.unit || 'шт',
|
||
imageUrl: updatedSupply.imageUrl,
|
||
warehouseStock: updatedSupply.currentStock || 0,
|
||
isAvailable: (updatedSupply.currentStock || 0) > 0,
|
||
warehouseConsumableId: updatedSupply.id,
|
||
createdAt: updatedSupply.createdAt,
|
||
updatedAt: updatedSupply.updatedAt,
|
||
organization: updatedSupply.organization,
|
||
}
|
||
|
||
console.warn('🔥 SUPPLY PRICE UPDATED:', {
|
||
id: transformedSupply.id,
|
||
name: transformedSupply.name,
|
||
oldPrice: existingSupply.price,
|
||
newPrice: transformedSupply.pricePerUnit,
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Цена расходника успешно обновлена',
|
||
supply: transformedSupply,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating supply price:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении цены расходника',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Использовать расходники фулфилмента
|
||
useFulfillmentSupplies: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
supplyId: string
|
||
quantityUsed: number
|
||
description?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Использование расходников доступно только для фулфилмент центров')
|
||
}
|
||
|
||
// Находим расходник
|
||
const existingSupply = await prisma.supply.findFirst({
|
||
where: {
|
||
id: args.input.supplyId,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingSupply) {
|
||
throw new GraphQLError('Расходник не найден или нет доступа')
|
||
}
|
||
|
||
// Проверяем, что достаточно расходников
|
||
if (existingSupply.currentStock < args.input.quantityUsed) {
|
||
throw new GraphQLError(
|
||
`Недостаточно расходников. Доступно: ${existingSupply.currentStock}, требуется: ${args.input.quantityUsed}`,
|
||
)
|
||
}
|
||
|
||
try {
|
||
// Обновляем количество расходников
|
||
const updatedSupply = await prisma.supply.update({
|
||
where: { id: args.input.supplyId },
|
||
data: {
|
||
currentStock: existingSupply.currentStock - args.input.quantityUsed,
|
||
updatedAt: new Date(),
|
||
},
|
||
include: { organization: true },
|
||
})
|
||
|
||
console.warn('🔧 Использованы расходники фулфилмента:', {
|
||
supplyName: updatedSupply.name,
|
||
quantityUsed: args.input.quantityUsed,
|
||
remainingStock: updatedSupply.currentStock,
|
||
description: args.input.description,
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: `Использовано ${args.input.quantityUsed} ${updatedSupply.unit} расходника "${updatedSupply.name}"`,
|
||
supply: updatedSupply,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error using fulfillment supplies:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при использовании расходников',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Создать заказ поставки расходников
|
||
// Два сценария:
|
||
// 1. Селлер → Поставщик → Фулфилмент (селлер заказывает для фулфилмент-центра)
|
||
// 2. Фулфилмент → Поставщик → Фулфилмент (фулфилмент заказывает для себя)
|
||
//
|
||
// Процесс: Заказчик → Поставщик → [Логистика] → Фулфилмент
|
||
// 1. Заказчик (селлер или фулфилмент) создает заказ у поставщика расходников
|
||
// 2. Поставщик получает заказ и готовит товары
|
||
// 3. Логистика транспортирует товары на склад фулфилмента
|
||
// 4. Фулфилмент принимает товары на склад
|
||
// 5. Расходники создаются в системе фулфилмент-центра
|
||
createSupplyOrder: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
partnerId: string
|
||
deliveryDate: string
|
||
fulfillmentCenterId?: string // ID фулфилмент-центра для доставки
|
||
logisticsPartnerId?: string // ID логистической компании
|
||
items: Array<{
|
||
productId: string
|
||
quantity: number
|
||
recipe?: {
|
||
services: string[]
|
||
fulfillmentConsumables: string[]
|
||
sellerConsumables: string[]
|
||
marketplaceCardId?: string
|
||
}
|
||
}>
|
||
notes?: string // Дополнительные заметки к заказу
|
||
consumableType?: string // Классификация расходников
|
||
}
|
||
},
|
||
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 },
|
||
})
|
||
|
||
console.warn('🔍 Проверка пользователя:', {
|
||
userId: context.user.id,
|
||
userFound: !!currentUser,
|
||
organizationFound: !!currentUser?.organization,
|
||
organizationType: currentUser?.organization?.type,
|
||
organizationId: currentUser?.organization?.id,
|
||
})
|
||
|
||
if (!currentUser) {
|
||
throw new GraphQLError('Пользователь не найден')
|
||
}
|
||
|
||
if (!currentUser.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем тип организации и определяем роль в процессе поставки
|
||
const allowedTypes = ['FULFILLMENT', 'SELLER', 'LOGIST']
|
||
if (!allowedTypes.includes(currentUser.organization.type)) {
|
||
throw new GraphQLError('Заказы поставок недоступны для данного типа организации')
|
||
}
|
||
|
||
// Определяем роль организации в процессе поставки
|
||
const organizationRole = currentUser.organization.type
|
||
let fulfillmentCenterId = args.input.fulfillmentCenterId
|
||
|
||
// Если заказ создает фулфилмент-центр, он сам является получателем
|
||
if (organizationRole === 'FULFILLMENT') {
|
||
fulfillmentCenterId = currentUser.organization.id
|
||
}
|
||
|
||
// Если указан фулфилмент-центр, проверяем его существование
|
||
if (fulfillmentCenterId) {
|
||
const fulfillmentCenter = await prisma.organization.findFirst({
|
||
where: {
|
||
id: fulfillmentCenterId,
|
||
type: 'FULFILLMENT',
|
||
},
|
||
})
|
||
|
||
if (!fulfillmentCenter) {
|
||
return {
|
||
success: false,
|
||
message: 'Указанный фулфилмент-центр не найден',
|
||
}
|
||
}
|
||
}
|
||
|
||
// Проверяем, что партнер существует и является поставщиком
|
||
const partner = await prisma.organization.findFirst({
|
||
where: {
|
||
id: args.input.partnerId,
|
||
type: 'WHOLESALE',
|
||
},
|
||
})
|
||
|
||
if (!partner) {
|
||
return {
|
||
success: false,
|
||
message: 'Партнер не найден или не является поставщиком',
|
||
}
|
||
}
|
||
|
||
// Проверяем, что партнер является контрагентом
|
||
const counterparty = await prisma.counterparty.findFirst({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterpartyId: args.input.partnerId,
|
||
},
|
||
})
|
||
|
||
if (!counterparty) {
|
||
return {
|
||
success: false,
|
||
message: 'Данная организация не является вашим партнером',
|
||
}
|
||
}
|
||
|
||
// Получаем товары для проверки наличия и цен
|
||
const productIds = args.input.items.map((item) => item.productId)
|
||
const products = await prisma.product.findMany({
|
||
where: {
|
||
id: { in: productIds },
|
||
organizationId: args.input.partnerId,
|
||
isActive: true,
|
||
},
|
||
})
|
||
|
||
if (products.length !== productIds.length) {
|
||
return {
|
||
success: false,
|
||
message: 'Некоторые товары не найдены или неактивны',
|
||
}
|
||
}
|
||
|
||
// Проверяем наличие товаров
|
||
for (const item of args.input.items) {
|
||
const product = products.find((p) => p.id === item.productId)
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: `Товар ${item.productId} не найден`,
|
||
}
|
||
}
|
||
if (product.quantity < item.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара "${product.name}". Доступно: ${product.quantity}, запрошено: ${item.quantity}`,
|
||
}
|
||
}
|
||
}
|
||
|
||
// Рассчитываем общую сумму и количество
|
||
let totalAmount = 0
|
||
let totalItems = 0
|
||
const orderItems = args.input.items.map((item) => {
|
||
const product = products.find((p) => p.id === item.productId)!
|
||
const itemTotal = Number(product.price) * item.quantity
|
||
totalAmount += itemTotal
|
||
totalItems += item.quantity
|
||
|
||
return {
|
||
productId: item.productId,
|
||
quantity: item.quantity,
|
||
price: product.price,
|
||
totalPrice: new Prisma.Decimal(itemTotal),
|
||
// Передача данных рецептуры в Prisma модель
|
||
services: item.recipe?.services || [],
|
||
fulfillmentConsumables: item.recipe?.fulfillmentConsumables || [],
|
||
sellerConsumables: item.recipe?.sellerConsumables || [],
|
||
marketplaceCardId: item.recipe?.marketplaceCardId,
|
||
}
|
||
})
|
||
|
||
try {
|
||
// Определяем начальный статус в зависимости от роли организации
|
||
let initialStatus: 'PENDING' | 'CONFIRMED' = 'PENDING'
|
||
if (organizationRole === 'SELLER') {
|
||
initialStatus = 'PENDING' // Селлер создает заказ, ждет подтверждения поставщика
|
||
} else if (organizationRole === 'FULFILLMENT') {
|
||
initialStatus = 'PENDING' // Фулфилмент заказывает для своего склада
|
||
} else if (organizationRole === 'LOGIST') {
|
||
initialStatus = 'CONFIRMED' // Логист может сразу подтверждать заказы
|
||
}
|
||
|
||
// Подготавливаем данные для создания заказа
|
||
const createData: any = {
|
||
partnerId: args.input.partnerId,
|
||
deliveryDate: new Date(args.input.deliveryDate),
|
||
totalAmount: new Prisma.Decimal(totalAmount),
|
||
totalItems: totalItems,
|
||
organizationId: currentUser.organization.id,
|
||
fulfillmentCenterId: fulfillmentCenterId,
|
||
consumableType: args.input.consumableType,
|
||
status: initialStatus,
|
||
items: {
|
||
create: orderItems,
|
||
},
|
||
}
|
||
|
||
// 🔄 ЛОГИСТИКА ОПЦИОНАЛЬНА: добавляем только если передана
|
||
if (args.input.logisticsPartnerId) {
|
||
createData.logisticsPartnerId = args.input.logisticsPartnerId
|
||
}
|
||
|
||
console.warn('🔍 Создаем SupplyOrder с данными:', {
|
||
hasLogistics: !!args.input.logisticsPartnerId,
|
||
logisticsId: args.input.logisticsPartnerId,
|
||
createData: createData,
|
||
})
|
||
|
||
const supplyOrder = await prisma.supplyOrder.create({
|
||
data: createData,
|
||
include: {
|
||
partner: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
fulfillmentCenter: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
logisticsPartner: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// 📦 РЕЗЕРВИРУЕМ ТОВАРЫ У ПОСТАВЩИКА
|
||
// Увеличиваем поле "ordered" для каждого заказанного товара
|
||
for (const item of args.input.items) {
|
||
await prisma.product.update({
|
||
where: { id: item.productId },
|
||
data: {
|
||
ordered: {
|
||
increment: item.quantity,
|
||
},
|
||
},
|
||
})
|
||
}
|
||
|
||
console.warn(
|
||
`📦 Зарезервированы товары для заказа ${supplyOrder.id}:`,
|
||
args.input.items.map((item) => `${item.productId}: +${item.quantity} шт.`).join(', '),
|
||
)
|
||
|
||
// Создаем расходники на основе заказанных товаров
|
||
// Расходники создаются в организации получателя (фулфилмент-центре)
|
||
const suppliesData = args.input.items.map((item) => {
|
||
const product = products.find((p) => p.id === item.productId)!
|
||
const productWithCategory = supplyOrder.items.find(
|
||
(orderItem: { productId: string; product: { category?: { name: string } | null } }) =>
|
||
orderItem.productId === item.productId,
|
||
)?.product
|
||
|
||
return {
|
||
name: product.name,
|
||
description: product.description || `Заказано у ${partner.name}`,
|
||
price: product.price, // Цена закупки у поставщика
|
||
quantity: item.quantity,
|
||
unit: 'шт',
|
||
category: productWithCategory?.category?.name || 'Расходники',
|
||
status: 'planned', // Статус "запланировано" (ожидает одобрения поставщиком)
|
||
date: new Date(args.input.deliveryDate),
|
||
supplier: partner.name || partner.fullName || 'Не указан',
|
||
minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток
|
||
currentStock: 0, // Пока товар не пришел
|
||
// Расходники создаются в организации получателя (фулфилмент-центре)
|
||
organizationId: fulfillmentCenterId || currentUser.organization!.id,
|
||
}
|
||
})
|
||
|
||
// Создаем расходники
|
||
await prisma.supply.createMany({
|
||
data: suppliesData,
|
||
})
|
||
|
||
// 🔔 ОТПРАВЛЯЕМ УВЕДОМЛЕНИЕ ПОСТАВЩИКУ О НОВОМ ЗАКАЗЕ
|
||
try {
|
||
const orderSummary = args.input.items
|
||
.map((item) => {
|
||
const product = products.find((p) => p.id === item.productId)!
|
||
return `${product.name} - ${item.quantity} шт.`
|
||
})
|
||
.join(', ')
|
||
|
||
const notificationMessage = `🔔 Новый заказ поставки от ${
|
||
currentUser.organization.name || currentUser.organization.fullName
|
||
}!\n\nТовары: ${orderSummary}\nДата доставки: ${new Date(args.input.deliveryDate).toLocaleDateString(
|
||
'ru-RU',
|
||
)}\nОбщая сумма: ${totalAmount.toLocaleString(
|
||
'ru-RU',
|
||
)} ₽\n\nПожалуйста, подтвердите заказ в разделе "Поставки".`
|
||
|
||
await prisma.message.create({
|
||
data: {
|
||
content: notificationMessage,
|
||
type: 'TEXT',
|
||
senderId: context.user.id,
|
||
senderOrganizationId: currentUser.organization.id,
|
||
receiverOrganizationId: args.input.partnerId,
|
||
},
|
||
})
|
||
|
||
console.warn(`✅ Уведомление отправлено поставщику ${partner.name}`)
|
||
} catch (notificationError) {
|
||
console.error('❌ Ошибка отправки уведомления:', notificationError)
|
||
// Не прерываем выполнение, если уведомление не отправилось
|
||
}
|
||
|
||
// Формируем сообщение в зависимости от роли организации
|
||
let successMessage = ''
|
||
if (organizationRole === 'SELLER') {
|
||
successMessage = `Заказ поставки расходников создан! Расходники будут доставлены ${
|
||
fulfillmentCenterId ? 'на указанный фулфилмент-склад' : 'согласно настройкам'
|
||
}. Ожидайте подтверждения от поставщика.`
|
||
} else if (organizationRole === 'FULFILLMENT') {
|
||
successMessage =
|
||
'Заказ поставки расходников создан для вашего склада! Ожидайте подтверждения от поставщика и координации с логистикой.'
|
||
} else if (organizationRole === 'LOGIST') {
|
||
successMessage =
|
||
'Заказ поставки создан и подтвержден! Координируйте доставку расходников от поставщика на фулфилмент-склад.'
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: successMessage,
|
||
order: supplyOrder,
|
||
processInfo: {
|
||
role: organizationRole,
|
||
supplier: partner.name || partner.fullName,
|
||
fulfillmentCenter: fulfillmentCenterId,
|
||
logistics: args.input.logisticsPartnerId,
|
||
status: initialStatus,
|
||
},
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Создать товар
|
||
createProduct: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
name: string
|
||
article: string
|
||
description?: string
|
||
price: number
|
||
pricePerSet?: number
|
||
quantity: number
|
||
setQuantity?: number
|
||
ordered?: number
|
||
inTransit?: number
|
||
stock?: number
|
||
sold?: number
|
||
type?: 'PRODUCT' | 'CONSUMABLE'
|
||
categoryId?: string
|
||
brand?: string
|
||
color?: string
|
||
size?: string
|
||
weight?: number
|
||
dimensions?: string
|
||
material?: string
|
||
images?: string[]
|
||
mainImage?: string
|
||
isActive?: boolean
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
console.warn('🆕 CREATE_PRODUCT RESOLVER - ВЫЗВАН:', {
|
||
hasUser: !!context.user,
|
||
userId: context.user?.id,
|
||
inputData: args.input,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это поставщик
|
||
if (currentUser.organization.type !== 'WHOLESALE') {
|
||
throw new GraphQLError('Товары доступны только для поставщиков')
|
||
}
|
||
|
||
// Проверяем уникальность артикула в рамках организации
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
article: args.input.article,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (existingProduct) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар с таким артикулом уже существует',
|
||
}
|
||
}
|
||
|
||
try {
|
||
console.warn('🛍️ СОЗДАНИЕ ТОВАРА - НАЧАЛО:', {
|
||
userId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
productData: {
|
||
name: args.input.name,
|
||
article: args.input.article,
|
||
type: args.input.type || 'PRODUCT',
|
||
isActive: args.input.isActive ?? true,
|
||
},
|
||
})
|
||
|
||
const product = await prisma.product.create({
|
||
data: {
|
||
name: args.input.name,
|
||
article: args.input.article,
|
||
description: args.input.description,
|
||
price: args.input.price,
|
||
pricePerSet: args.input.pricePerSet,
|
||
quantity: args.input.quantity,
|
||
setQuantity: args.input.setQuantity,
|
||
ordered: args.input.ordered,
|
||
inTransit: args.input.inTransit,
|
||
stock: args.input.stock,
|
||
sold: args.input.sold,
|
||
type: args.input.type || 'PRODUCT',
|
||
categoryId: args.input.categoryId,
|
||
brand: args.input.brand,
|
||
color: args.input.color,
|
||
size: args.input.size,
|
||
weight: args.input.weight,
|
||
dimensions: args.input.dimensions,
|
||
material: args.input.material,
|
||
images: JSON.stringify(args.input.images || []),
|
||
mainImage: args.input.mainImage,
|
||
isActive: args.input.isActive ?? true,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
console.warn('✅ ТОВАР УСПЕШНО СОЗДАН:', {
|
||
productId: product.id,
|
||
name: product.name,
|
||
article: product.article,
|
||
type: product.type,
|
||
isActive: product.isActive,
|
||
organizationId: product.organizationId,
|
||
createdAt: product.createdAt,
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар успешно создан',
|
||
product,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating product:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании товара',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить товар
|
||
updateProduct: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
input: {
|
||
name: string
|
||
article: string
|
||
description?: string
|
||
price: number
|
||
pricePerSet?: number
|
||
quantity: number
|
||
setQuantity?: number
|
||
ordered?: number
|
||
inTransit?: number
|
||
stock?: number
|
||
sold?: number
|
||
type?: 'PRODUCT' | 'CONSUMABLE'
|
||
categoryId?: string
|
||
brand?: string
|
||
color?: string
|
||
size?: string
|
||
weight?: number
|
||
dimensions?: string
|
||
material?: string
|
||
images?: string[]
|
||
mainImage?: string
|
||
isActive?: boolean
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что товар принадлежит текущей организации
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingProduct) {
|
||
throw new GraphQLError('Товар не найден или нет доступа')
|
||
}
|
||
|
||
// Проверяем уникальность артикула (если он изменился)
|
||
if (args.input.article !== existingProduct.article) {
|
||
const duplicateProduct = await prisma.product.findFirst({
|
||
where: {
|
||
article: args.input.article,
|
||
organizationId: currentUser.organization.id,
|
||
NOT: { id: args.id },
|
||
},
|
||
})
|
||
|
||
if (duplicateProduct) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар с таким артикулом уже существует',
|
||
}
|
||
}
|
||
}
|
||
|
||
try {
|
||
const product = await prisma.product.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
article: args.input.article,
|
||
description: args.input.description,
|
||
price: args.input.price,
|
||
pricePerSet: args.input.pricePerSet,
|
||
quantity: args.input.quantity,
|
||
setQuantity: args.input.setQuantity,
|
||
ordered: args.input.ordered,
|
||
inTransit: args.input.inTransit,
|
||
stock: args.input.stock,
|
||
sold: args.input.sold,
|
||
...(args.input.type && { type: args.input.type }),
|
||
categoryId: args.input.categoryId,
|
||
brand: args.input.brand,
|
||
color: args.input.color,
|
||
size: args.input.size,
|
||
weight: args.input.weight,
|
||
dimensions: args.input.dimensions,
|
||
material: args.input.material,
|
||
images: args.input.images ? JSON.stringify(args.input.images) : undefined,
|
||
mainImage: args.input.mainImage,
|
||
isActive: args.input.isActive ?? true,
|
||
},
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар успешно обновлен',
|
||
product,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating product:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении товара',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Проверка уникальности артикула
|
||
checkArticleUniqueness: async (_: unknown, args: { article: string; excludeId?: string }, context: Context) => {
|
||
const { currentUser, prisma } = context
|
||
|
||
if (!currentUser?.organization?.id) {
|
||
return {
|
||
isUnique: false,
|
||
existingProduct: null,
|
||
}
|
||
}
|
||
|
||
try {
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
article: args.article,
|
||
organizationId: currentUser.organization.id,
|
||
...(args.excludeId && { id: { not: args.excludeId } }),
|
||
},
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
article: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
isUnique: !existingProduct,
|
||
existingProduct,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error checking article uniqueness:', error)
|
||
return {
|
||
isUnique: false,
|
||
existingProduct: null,
|
||
}
|
||
}
|
||
},
|
||
|
||
// Резервирование товара при создании заказа
|
||
reserveProductStock: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
|
||
const { currentUser, prisma } = context
|
||
|
||
if (!currentUser?.organization?.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Необходимо авторизоваться',
|
||
}
|
||
}
|
||
|
||
try {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: args.productId },
|
||
})
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден',
|
||
}
|
||
}
|
||
|
||
// Проверяем доступность товара
|
||
const availableStock = (product.stock || product.quantity) - (product.ordered || 0)
|
||
if (availableStock < args.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара на складе. Доступно: ${availableStock}, запрошено: ${args.quantity}`,
|
||
}
|
||
}
|
||
|
||
// Резервируем товар (увеличиваем поле ordered)
|
||
const updatedProduct = await prisma.product.update({
|
||
where: { id: args.productId },
|
||
data: {
|
||
ordered: (product.ordered || 0) + args.quantity,
|
||
},
|
||
})
|
||
|
||
console.warn(`📦 Зарезервировано ${args.quantity} единиц товара ${product.name}`)
|
||
|
||
return {
|
||
success: true,
|
||
message: `Зарезервировано ${args.quantity} единиц товара`,
|
||
product: updatedProduct,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error reserving product stock:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при резервировании товара',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Освобождение резерва при отмене заказа
|
||
releaseProductReserve: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
|
||
const { currentUser, prisma } = context
|
||
|
||
if (!currentUser?.organization?.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Необходимо авторизоваться',
|
||
}
|
||
}
|
||
|
||
try {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: args.productId },
|
||
})
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден',
|
||
}
|
||
}
|
||
|
||
// Освобождаем резерв (уменьшаем поле ordered)
|
||
const newOrdered = Math.max((product.ordered || 0) - args.quantity, 0)
|
||
|
||
const updatedProduct = await prisma.product.update({
|
||
where: { id: args.productId },
|
||
data: {
|
||
ordered: newOrdered,
|
||
},
|
||
})
|
||
|
||
console.warn(`🔄 Освобожден резерв ${args.quantity} единиц товара ${product.name}`)
|
||
|
||
return {
|
||
success: true,
|
||
message: `Освобожден резерв ${args.quantity} единиц товара`,
|
||
product: updatedProduct,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error releasing product reserve:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при освобождении резерва',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновление статуса "в пути"
|
||
updateProductInTransit: async (
|
||
_: unknown,
|
||
args: { productId: string; quantity: number; operation: string },
|
||
context: Context,
|
||
) => {
|
||
const { currentUser, prisma } = context
|
||
|
||
if (!currentUser?.organization?.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Необходимо авторизоваться',
|
||
}
|
||
}
|
||
|
||
try {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: args.productId },
|
||
})
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден',
|
||
}
|
||
}
|
||
|
||
let newInTransit = product.inTransit || 0
|
||
let newOrdered = product.ordered || 0
|
||
|
||
if (args.operation === 'ship') {
|
||
// При отгрузке: переводим из "заказано" в "в пути"
|
||
newInTransit = (product.inTransit || 0) + args.quantity
|
||
newOrdered = Math.max((product.ordered || 0) - args.quantity, 0)
|
||
} else if (args.operation === 'deliver') {
|
||
// При доставке: убираем из "в пути", добавляем в "продано"
|
||
newInTransit = Math.max((product.inTransit || 0) - args.quantity, 0)
|
||
}
|
||
|
||
const updatedProduct = await prisma.product.update({
|
||
where: { id: args.productId },
|
||
data: {
|
||
inTransit: newInTransit,
|
||
ordered: newOrdered,
|
||
...(args.operation === 'deliver' && {
|
||
sold: (product.sold || 0) + args.quantity,
|
||
}),
|
||
},
|
||
})
|
||
|
||
console.warn(`🚚 Обновлен статус "в пути" для товара ${product.name}: ${args.operation}`)
|
||
|
||
return {
|
||
success: true,
|
||
message: `Статус товара обновлен: ${args.operation}`,
|
||
product: updatedProduct,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating product in transit:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении статуса товара',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить товар
|
||
deleteProduct: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что товар принадлежит текущей организации
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingProduct) {
|
||
throw new GraphQLError('Товар не найден или нет доступа')
|
||
}
|
||
|
||
try {
|
||
await prisma.product.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error deleting product:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Создать категорию
|
||
createCategory: async (_: unknown, args: { input: { name: string } }, context: Context) => {
|
||
if (!context.user && !context.admin) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
// Проверяем уникальность названия категории
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { name: args.input.name },
|
||
})
|
||
|
||
if (existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория с таким названием уже существует',
|
||
}
|
||
}
|
||
|
||
try {
|
||
const category = await prisma.category.create({
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Категория успешно создана',
|
||
category,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating category:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании категории',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить категорию
|
||
updateCategory: async (_: unknown, args: { id: string; input: { name: string } }, context: Context) => {
|
||
if (!context.user && !context.admin) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
// Проверяем существование категории
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
if (!existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория не найдена',
|
||
}
|
||
}
|
||
|
||
// Проверяем уникальность нового названия (если изменилось)
|
||
if (args.input.name !== existingCategory.name) {
|
||
const duplicateCategory = await prisma.category.findUnique({
|
||
where: { name: args.input.name },
|
||
})
|
||
|
||
if (duplicateCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория с таким названием уже существует',
|
||
}
|
||
}
|
||
}
|
||
|
||
try {
|
||
const category = await prisma.category.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Категория успешно обновлена',
|
||
category,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating category:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении категории',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить категорию
|
||
deleteCategory: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user && !context.admin) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
// Проверяем существование категории
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
include: { products: true },
|
||
})
|
||
|
||
if (!existingCategory) {
|
||
throw new GraphQLError('Категория не найдена')
|
||
}
|
||
|
||
// Проверяем, есть ли товары в этой категории
|
||
if (existingCategory.products.length > 0) {
|
||
throw new GraphQLError('Нельзя удалить категорию, в которой есть товары')
|
||
}
|
||
|
||
try {
|
||
await prisma.category.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error deleting category:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Добавить товар в корзину
|
||
addToCart: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что товар существует и активен
|
||
const product = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.productId,
|
||
isActive: true,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден или неактивен',
|
||
}
|
||
}
|
||
|
||
// Проверяем, что пользователь не пытается добавить свой собственный товар
|
||
if (product.organizationId === currentUser.organization.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Нельзя добавлять собственные товары в корзину',
|
||
}
|
||
}
|
||
|
||
// Найти или создать корзину
|
||
let cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
})
|
||
|
||
if (!cart) {
|
||
cart = await prisma.cart.create({
|
||
data: {
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Проверяем, есть ли уже такой товар в корзине
|
||
const existingCartItem = await prisma.cartItem.findUnique({
|
||
where: {
|
||
cartId_productId: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
})
|
||
|
||
if (existingCartItem) {
|
||
// Обновляем количество
|
||
const newQuantity = existingCartItem.quantity + args.quantity
|
||
|
||
if (newQuantity > product.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
|
||
}
|
||
}
|
||
|
||
await prisma.cartItem.update({
|
||
where: { id: existingCartItem.id },
|
||
data: { quantity: newQuantity },
|
||
})
|
||
} else {
|
||
// Создаем новый элемент корзины
|
||
if (args.quantity > product.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
|
||
}
|
||
}
|
||
|
||
await prisma.cartItem.create({
|
||
data: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
quantity: args.quantity,
|
||
},
|
||
})
|
||
}
|
||
|
||
// Возвращаем обновленную корзину
|
||
const updatedCart = await prisma.cart.findUnique({
|
||
where: { id: cart.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар добавлен в корзину',
|
||
cart: updatedCart,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding to cart:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при добавлении в корзину',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить количество товара в корзине
|
||
updateCartItem: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
const cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
})
|
||
|
||
if (!cart) {
|
||
return {
|
||
success: false,
|
||
message: 'Корзина не найдена',
|
||
}
|
||
}
|
||
|
||
// Проверяем, что товар существует в корзине
|
||
const cartItem = await prisma.cartItem.findUnique({
|
||
where: {
|
||
cartId_productId: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
include: {
|
||
product: true,
|
||
},
|
||
})
|
||
|
||
if (!cartItem) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден в корзине',
|
||
}
|
||
}
|
||
|
||
if (args.quantity <= 0) {
|
||
return {
|
||
success: false,
|
||
message: 'Количество должно быть больше 0',
|
||
}
|
||
}
|
||
|
||
if (args.quantity > cartItem.product.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}`,
|
||
}
|
||
}
|
||
|
||
try {
|
||
await prisma.cartItem.update({
|
||
where: { id: cartItem.id },
|
||
data: { quantity: args.quantity },
|
||
})
|
||
|
||
// Возвращаем обновленную корзину
|
||
const updatedCart = await prisma.cart.findUnique({
|
||
where: { id: cart.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Количество товара обновлено',
|
||
cart: updatedCart,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating cart item:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении корзины',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить товар из корзины
|
||
removeFromCart: async (_: unknown, args: { productId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
const cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
})
|
||
|
||
if (!cart) {
|
||
return {
|
||
success: false,
|
||
message: 'Корзина не найдена',
|
||
}
|
||
}
|
||
|
||
try {
|
||
await prisma.cartItem.delete({
|
||
where: {
|
||
cartId_productId: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
})
|
||
|
||
// Возвращаем обновленную корзину
|
||
const updatedCart = await prisma.cart.findUnique({
|
||
where: { id: cart.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар удален из корзины',
|
||
cart: updatedCart,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error removing from cart:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при удалении из корзины',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Очистить корзину
|
||
clearCart: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
const cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
})
|
||
|
||
if (!cart) {
|
||
return false
|
||
}
|
||
|
||
try {
|
||
await prisma.cartItem.deleteMany({
|
||
where: { cartId: cart.id },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error clearing cart:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Добавить товар в избранное
|
||
addToFavorites: async (_: unknown, args: { productId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что товар существует и активен
|
||
const product = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.productId,
|
||
isActive: true,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден или неактивен',
|
||
}
|
||
}
|
||
|
||
// Проверяем, что пользователь не пытается добавить свой собственный товар
|
||
if (product.organizationId === currentUser.organization.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Нельзя добавлять собственные товары в избранное',
|
||
}
|
||
}
|
||
|
||
try {
|
||
// Проверяем, есть ли уже такой товар в избранном
|
||
const existingFavorite = await prisma.favorites.findUnique({
|
||
where: {
|
||
organizationId_productId: {
|
||
organizationId: currentUser.organization.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
})
|
||
|
||
if (existingFavorite) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар уже в избранном',
|
||
}
|
||
}
|
||
|
||
// Добавляем товар в избранное
|
||
await prisma.favorites.create({
|
||
data: {
|
||
organizationId: currentUser.organization.id,
|
||
productId: args.productId,
|
||
},
|
||
})
|
||
|
||
// Возвращаем обновленный список избранного
|
||
const favorites = await prisma.favorites.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар добавлен в избранное',
|
||
favorites: favorites.map((favorite) => favorite.product),
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding to favorites:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при добавлении в избранное',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить товар из избранного
|
||
removeFromFavorites: async (_: unknown, args: { productId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Удаляем товар из избранного
|
||
await prisma.favorites.deleteMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
productId: args.productId,
|
||
},
|
||
})
|
||
|
||
// Возвращаем обновленный список избранного
|
||
const favorites = await prisma.favorites.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар удален из избранного',
|
||
favorites: favorites.map((favorite) => favorite.product),
|
||
}
|
||
} catch (error) {
|
||
console.error('Error removing from favorites:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при удалении из избранного',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Создать сотрудника
|
||
createEmployee: async (_: unknown, args: { input: CreateEmployeeInput }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
const employee = await prisma.employee.create({
|
||
data: {
|
||
...args.input,
|
||
organizationId: currentUser.organization.id,
|
||
birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined,
|
||
passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined,
|
||
hireDate: new Date(args.input.hireDate),
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Сотрудник успешно добавлен',
|
||
employee,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating employee:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании сотрудника',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить сотрудника
|
||
updateEmployee: async (_: unknown, args: { id: string; input: UpdateEmployeeInput }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
const employee = await prisma.employee.update({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
data: {
|
||
...args.input,
|
||
birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined,
|
||
passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined,
|
||
hireDate: args.input.hireDate ? new Date(args.input.hireDate) : undefined,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Сотрудник успешно обновлен',
|
||
employee,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating employee:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении сотрудника',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить сотрудника
|
||
deleteEmployee: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
await prisma.employee.delete({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error deleting employee:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Обновить табель сотрудника
|
||
updateEmployeeSchedule: async (_: unknown, args: { input: UpdateScheduleInput }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
// Проверяем что сотрудник принадлежит организации
|
||
const employee = await prisma.employee.findFirst({
|
||
where: {
|
||
id: args.input.employeeId,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!employee) {
|
||
throw new GraphQLError('Сотрудник не найден')
|
||
}
|
||
|
||
// Создаем или обновляем запись табеля
|
||
await prisma.employeeSchedule.upsert({
|
||
where: {
|
||
employeeId_date: {
|
||
employeeId: args.input.employeeId,
|
||
date: new Date(args.input.date),
|
||
},
|
||
},
|
||
create: {
|
||
employeeId: args.input.employeeId,
|
||
date: new Date(args.input.date),
|
||
status: args.input.status,
|
||
hoursWorked: args.input.hoursWorked,
|
||
overtimeHours: args.input.overtimeHours,
|
||
notes: args.input.notes,
|
||
},
|
||
update: {
|
||
status: args.input.status,
|
||
hoursWorked: args.input.hoursWorked,
|
||
overtimeHours: args.input.overtimeHours,
|
||
notes: args.input.notes,
|
||
},
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error updating employee schedule:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Создать поставку Wildberries
|
||
createWildberriesSupply: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
cards: Array<{
|
||
price: number
|
||
discountedPrice?: number
|
||
selectedQuantity: number
|
||
selectedServices?: string[]
|
||
}>
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Пока что просто логируем данные, так как таблицы еще нет
|
||
console.warn('Создание поставки Wildberries с данными:', args.input)
|
||
|
||
const totalAmount = args.input.cards.reduce((sum: number, card) => {
|
||
const cardPrice = card.discountedPrice || card.price
|
||
const servicesPrice = (card.selectedServices?.length || 0) * 50
|
||
return sum + (cardPrice + servicesPrice) * card.selectedQuantity
|
||
}, 0)
|
||
|
||
const totalItems = args.input.cards.reduce((sum: number, card) => sum + card.selectedQuantity, 0)
|
||
|
||
// Временная заглушка - вернем success без создания в БД
|
||
return {
|
||
success: true,
|
||
message: `Поставка создана успешно! Товаров: ${totalItems}, Сумма: ${totalAmount} руб.`,
|
||
supply: null, // Временно null
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating Wildberries supply:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании поставки Wildberries',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Создать поставщика для поставки
|
||
createSupplySupplier: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
name: string
|
||
contactName: string
|
||
phone: string
|
||
market?: string
|
||
address?: string
|
||
place?: string
|
||
telegram?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Создаем поставщика в базе данных
|
||
const supplier = await prisma.supplySupplier.create({
|
||
data: {
|
||
name: args.input.name,
|
||
contactName: args.input.contactName,
|
||
phone: args.input.phone,
|
||
market: args.input.market,
|
||
address: args.input.address,
|
||
place: args.input.place,
|
||
telegram: args.input.telegram,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Поставщик добавлен успешно!',
|
||
supplier: {
|
||
id: supplier.id,
|
||
name: supplier.name,
|
||
contactName: supplier.contactName,
|
||
phone: supplier.phone,
|
||
market: supplier.market,
|
||
address: supplier.address,
|
||
place: supplier.place,
|
||
telegram: supplier.telegram,
|
||
createdAt: supplier.createdAt,
|
||
},
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating supply supplier:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при добавлении поставщика',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить статус заказа поставки
|
||
updateSupplyOrderStatus: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
status:
|
||
| 'PENDING'
|
||
| 'CONFIRMED'
|
||
| 'IN_TRANSIT'
|
||
| 'SUPPLIER_APPROVED'
|
||
| 'LOGISTICS_CONFIRMED'
|
||
| 'SHIPPED'
|
||
| 'DELIVERED'
|
||
| 'CANCELLED'
|
||
},
|
||
context: Context,
|
||
) => {
|
||
console.warn(`[DEBUG] updateSupplyOrderStatus вызван для заказа ${args.id} со статусом ${args.status}`)
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Находим заказ поставки
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
OR: [
|
||
{ organizationId: currentUser.organization.id }, // Создатель заказа
|
||
{ partnerId: currentUser.organization.id }, // Поставщик
|
||
{ fulfillmentCenterId: currentUser.organization.id }, // Фулфилмент-центр
|
||
],
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
partner: true,
|
||
fulfillmentCenter: true,
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
throw new GraphQLError('Заказ поставки не найден или нет доступа')
|
||
}
|
||
|
||
// Обновляем статус заказа
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: args.status },
|
||
include: {
|
||
partner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// ОТКЛЮЧЕНО: Устаревшая логика для обновления расходников
|
||
// Теперь используются специальные мутации для каждой роли
|
||
const targetOrganizationId = existingOrder.fulfillmentCenterId || existingOrder.organizationId
|
||
|
||
if (args.status === 'CONFIRMED') {
|
||
console.warn(`[WARNING] Попытка использовать устаревший статус CONFIRMED для заказа ${args.id}`)
|
||
// Не обновляем расходники для устаревших статусов
|
||
// await prisma.supply.updateMany({
|
||
// where: {
|
||
// organizationId: targetOrganizationId,
|
||
// status: "planned",
|
||
// name: {
|
||
// in: existingOrder.items.map(item => item.product.name)
|
||
// }
|
||
// },
|
||
// data: {
|
||
// status: "confirmed"
|
||
// }
|
||
// });
|
||
|
||
console.warn("✅ Статусы расходников обновлены на 'confirmed'")
|
||
}
|
||
|
||
if (args.status === 'IN_TRANSIT') {
|
||
// При отгрузке - переводим расходники в статус "in-transit"
|
||
await prisma.supply.updateMany({
|
||
where: {
|
||
organizationId: targetOrganizationId,
|
||
status: 'confirmed',
|
||
name: {
|
||
in: existingOrder.items.map((item) => item.product.name),
|
||
},
|
||
},
|
||
data: {
|
||
status: 'in-transit',
|
||
},
|
||
})
|
||
|
||
console.warn("✅ Статусы расходников обновлены на 'in-transit'")
|
||
}
|
||
|
||
// Если статус изменился на DELIVERED, обновляем склад
|
||
if (args.status === 'DELIVERED') {
|
||
console.warn('🚚 Обновляем склад организации:', {
|
||
targetOrganizationId,
|
||
fulfillmentCenterId: existingOrder.fulfillmentCenterId,
|
||
organizationId: existingOrder.organizationId,
|
||
itemsCount: existingOrder.items.length,
|
||
items: existingOrder.items.map((item) => ({
|
||
productName: item.product.name,
|
||
quantity: item.quantity,
|
||
})),
|
||
})
|
||
|
||
// 🔄 СИНХРОНИЗАЦИЯ: Обновляем товары поставщика (переводим из "в пути" в "продано" + обновляем основные остатки)
|
||
for (const item of existingOrder.items) {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: item.product.id },
|
||
})
|
||
|
||
if (product) {
|
||
// ИСПРАВЛЕНО: НЕ списываем повторно, только переводим из inTransit в sold
|
||
// Остаток уже был уменьшен при создании/одобрении заказа
|
||
await prisma.product.update({
|
||
where: { id: item.product.id },
|
||
data: {
|
||
// НЕ ТРОГАЕМ stock - он уже правильно уменьшен при заказе
|
||
// Только переводим из inTransit в sold
|
||
inTransit: Math.max((product.inTransit || 0) - item.quantity, 0),
|
||
sold: (product.sold || 0) + item.quantity,
|
||
},
|
||
})
|
||
console.warn(
|
||
`✅ Товар поставщика "${product.name}" обновлен: доставлено ${
|
||
item.quantity
|
||
} единиц (остаток НЕ ИЗМЕНЕН: ${product.stock || product.quantity || 0})`,
|
||
)
|
||
}
|
||
}
|
||
|
||
// Обновляем расходники
|
||
for (const item of existingOrder.items) {
|
||
console.warn('📦 Обрабатываем товар:', {
|
||
productName: item.product.name,
|
||
quantity: item.quantity,
|
||
targetOrganizationId,
|
||
})
|
||
|
||
// Ищем существующий расходник в правильной организации
|
||
const existingSupply = await prisma.supply.findFirst({
|
||
where: {
|
||
name: item.product.name,
|
||
organizationId: targetOrganizationId,
|
||
},
|
||
})
|
||
|
||
console.warn('🔍 Найден существующий расходник:', !!existingSupply)
|
||
|
||
if (existingSupply) {
|
||
console.warn('📈 Обновляем существующий расходник:', {
|
||
id: existingSupply.id,
|
||
oldStock: existingSupply.currentStock,
|
||
newStock: existingSupply.currentStock + item.quantity,
|
||
})
|
||
|
||
// Обновляем количество существующего расходника
|
||
await prisma.supply.update({
|
||
where: { id: existingSupply.id },
|
||
data: {
|
||
currentStock: existingSupply.currentStock + item.quantity,
|
||
status: 'in-stock', // Меняем статус на "на складе"
|
||
},
|
||
})
|
||
} else {
|
||
console.warn('➕ Создаем новый расходник:', {
|
||
name: item.product.name,
|
||
quantity: item.quantity,
|
||
organizationId: targetOrganizationId,
|
||
})
|
||
|
||
// Создаем новый расходник
|
||
const newSupply = await prisma.supply.create({
|
||
data: {
|
||
name: item.product.name,
|
||
description: item.product.description || `Поставка от ${existingOrder.partner.name}`,
|
||
price: item.price, // Цена закупки у поставщика
|
||
quantity: item.quantity,
|
||
unit: 'шт',
|
||
category: item.product.category?.name || 'Расходники',
|
||
status: 'in-stock',
|
||
date: new Date(),
|
||
supplier: existingOrder.partner.name || existingOrder.partner.fullName || 'Не указан',
|
||
minStock: Math.round(item.quantity * 0.1),
|
||
currentStock: item.quantity,
|
||
organizationId: targetOrganizationId,
|
||
},
|
||
})
|
||
|
||
console.warn('✅ Создан новый расходник:', {
|
||
id: newSupply.id,
|
||
name: newSupply.name,
|
||
currentStock: newSupply.currentStock,
|
||
})
|
||
}
|
||
}
|
||
|
||
console.warn('🎉 Склад организации успешно обновлен!')
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: `Статус заказа поставки обновлен на "${args.status}"`,
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating supply order status:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении статуса заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Назначение логистики фулфилментом на заказ селлера
|
||
assignLogisticsToSupply: async (
|
||
_: unknown,
|
||
args: {
|
||
supplyOrderId: string
|
||
logisticsPartnerId: string
|
||
responsibleId?: string
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что пользователь - фулфилмент
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Только фулфилмент может назначать логистику')
|
||
}
|
||
|
||
try {
|
||
// Находим заказ
|
||
const existingOrder = await prisma.supplyOrder.findUnique({
|
||
where: { id: args.supplyOrderId },
|
||
include: {
|
||
partner: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
throw new GraphQLError('Заказ поставки не найден')
|
||
}
|
||
|
||
// Проверяем, что это заказ для нашего фулфилмент-центра
|
||
if (existingOrder.fulfillmentCenterId !== currentUser.organization.id) {
|
||
throw new GraphQLError('Нет доступа к этому заказу')
|
||
}
|
||
|
||
// Проверяем, что статус позволяет назначить логистику
|
||
if (existingOrder.status !== 'SUPPLIER_APPROVED') {
|
||
throw new GraphQLError(`Нельзя назначить логистику для заказа со статусом ${existingOrder.status}`)
|
||
}
|
||
|
||
// Проверяем, что логистическая компания существует
|
||
const logisticsPartner = await prisma.organization.findUnique({
|
||
where: { id: args.logisticsPartnerId },
|
||
})
|
||
|
||
if (!logisticsPartner || logisticsPartner.type !== 'LOGIST') {
|
||
throw new GraphQLError('Логистическая компания не найдена')
|
||
}
|
||
|
||
// Обновляем заказ
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.supplyOrderId },
|
||
data: {
|
||
logisticsPartner: {
|
||
connect: { id: args.logisticsPartnerId },
|
||
},
|
||
status: 'CONFIRMED', // Переводим в статус "подтвержден фулфилментом"
|
||
},
|
||
include: {
|
||
partner: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
console.warn(`✅ Логистика назначена на заказ ${args.supplyOrderId}:`, {
|
||
logisticsPartner: logisticsPartner.name,
|
||
responsible: args.responsibleId,
|
||
newStatus: 'CONFIRMED',
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Логистика успешно назначена',
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Ошибка при назначении логистики:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка при назначении логистики',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Резолверы для новых действий с заказами поставок
|
||
supplierApproveOrder: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что пользователь - поставщик этого заказа
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
partnerId: currentUser.organization.id, // Только поставщик может одобрить
|
||
status: 'PENDING', // Можно одобрить только заказы в статусе PENDING
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для одобрения',
|
||
}
|
||
}
|
||
|
||
console.warn(`[DEBUG] Поставщик ${currentUser.organization.name} одобряет заказ ${args.id}`)
|
||
|
||
// 🔄 СИНХРОНИЗАЦИЯ ОСТАТКОВ: Резервируем товары у поставщика
|
||
const orderWithItems = await prisma.supplyOrder.findUnique({
|
||
where: { id: args.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (orderWithItems) {
|
||
for (const item of orderWithItems.items) {
|
||
// Резервируем товар (увеличиваем поле ordered)
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: item.product.id },
|
||
})
|
||
|
||
if (product) {
|
||
const availableStock = (product.stock || product.quantity) - (product.ordered || 0)
|
||
|
||
if (availableStock < item.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара "${product.name}" на складе. Доступно: ${availableStock}, требуется: ${item.quantity}`,
|
||
}
|
||
}
|
||
|
||
// Согласно правилам: при одобрении заказа остаток должен уменьшиться
|
||
const currentStock = product.stock || product.quantity || 0
|
||
const newStock = Math.max(currentStock - item.quantity, 0)
|
||
|
||
await prisma.product.update({
|
||
where: { id: item.product.id },
|
||
data: {
|
||
// Уменьшаем основной остаток (товар зарезервирован для заказа)
|
||
stock: newStock,
|
||
quantity: newStock, // Синхронизируем оба поля для совместимости
|
||
// Увеличиваем количество заказанного (для отслеживания)
|
||
ordered: (product.ordered || 0) + item.quantity,
|
||
},
|
||
})
|
||
|
||
console.warn(`📦 Товар "${product.name}" зарезервирован: ${item.quantity} единиц`)
|
||
console.warn(` 📊 Остаток: ${currentStock} -> ${newStock} (уменьшен на ${item.quantity})`)
|
||
console.warn(` 📋 Заказано: ${product.ordered || 0} -> ${(product.ordered || 0) + item.quantity}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'SUPPLIER_APPROVED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
console.warn(`[DEBUG] Заказ ${args.id} успешно обновлен до статуса: ${updatedOrder.status}`)
|
||
return {
|
||
success: true,
|
||
message: 'Заказ поставки одобрен поставщиком. Товары зарезервированы, остатки обновлены.',
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error approving supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при одобрении заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
supplierRejectOrder: async (_: unknown, args: { id: string; reason?: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
partnerId: currentUser.organization.id,
|
||
status: 'PENDING',
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для отклонения',
|
||
}
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'CANCELLED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// 📦 СНИМАЕМ РЕЗЕРВАЦИЮ ПРИ ОТКЛОНЕНИИ
|
||
// Восстанавливаем остатки и убираем резервацию для каждого отклоненного товара
|
||
for (const item of updatedOrder.items) {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: item.productId },
|
||
})
|
||
|
||
if (product) {
|
||
// Восстанавливаем основные остатки (на случай, если заказ был одобрен, а затем отклонен)
|
||
const currentStock = product.stock || product.quantity || 0
|
||
const restoredStock = currentStock + item.quantity
|
||
|
||
await prisma.product.update({
|
||
where: { id: item.productId },
|
||
data: {
|
||
// Восстанавливаем основной остаток
|
||
stock: restoredStock,
|
||
quantity: restoredStock,
|
||
// Уменьшаем количество заказанного
|
||
ordered: Math.max((product.ordered || 0) - item.quantity, 0),
|
||
},
|
||
})
|
||
|
||
console.warn(
|
||
`🔄 Восстановлены остатки товара "${product.name}": ${currentStock} -> ${restoredStock}, ordered: ${
|
||
product.ordered
|
||
} -> ${Math.max((product.ordered || 0) - item.quantity, 0)}`,
|
||
)
|
||
}
|
||
}
|
||
|
||
console.warn(
|
||
`📦 Снята резервация при отклонении заказа ${updatedOrder.id}:`,
|
||
updatedOrder.items.map((item) => `${item.productId}: -${item.quantity} шт.`).join(', '),
|
||
)
|
||
|
||
return {
|
||
success: true,
|
||
message: args.reason ? `Заказ отклонен поставщиком. Причина: ${args.reason}` : 'Заказ отклонен поставщиком',
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error rejecting supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отклонении заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
supplierShipOrder: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
partnerId: currentUser.organization.id,
|
||
status: 'LOGISTICS_CONFIRMED',
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для отправки',
|
||
}
|
||
}
|
||
|
||
// 🔄 СИНХРОНИЗАЦИЯ ОСТАТКОВ: Переводим товары из "заказано" в "в пути"
|
||
const orderWithItems = await prisma.supplyOrder.findUnique({
|
||
where: { id: args.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (orderWithItems) {
|
||
for (const item of orderWithItems.items) {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: item.product.id },
|
||
})
|
||
|
||
if (product) {
|
||
await prisma.product.update({
|
||
where: { id: item.product.id },
|
||
data: {
|
||
ordered: Math.max((product.ordered || 0) - item.quantity, 0),
|
||
inTransit: (product.inTransit || 0) + item.quantity,
|
||
},
|
||
})
|
||
|
||
console.warn(`🚚 Товар "${product.name}" переведен в статус "в пути": ${item.quantity} единиц`)
|
||
}
|
||
}
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'SHIPPED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: "Заказ отправлен поставщиком. Товары переведены в статус 'в пути'.",
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error shipping supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отправке заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
logisticsConfirmOrder: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
logisticsPartnerId: currentUser.organization.id,
|
||
OR: [{ status: 'SUPPLIER_APPROVED' }, { status: 'CONFIRMED' }],
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для подтверждения логистикой',
|
||
}
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'LOGISTICS_CONFIRMED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Заказ подтвержден логистической компанией',
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error confirming supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при подтверждении заказа логистикой',
|
||
}
|
||
}
|
||
},
|
||
|
||
logisticsRejectOrder: async (_: unknown, args: { id: string; reason?: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
logisticsPartnerId: currentUser.organization.id,
|
||
OR: [{ status: 'SUPPLIER_APPROVED' }, { status: 'CONFIRMED' }],
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для отклонения логистикой',
|
||
}
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'CANCELLED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: args.reason
|
||
? `Заказ отклонен логистической компанией. Причина: ${args.reason}`
|
||
: 'Заказ отклонен логистической компанией',
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error rejecting supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отклонении заказа логистикой',
|
||
}
|
||
}
|
||
},
|
||
|
||
fulfillmentReceiveOrder: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
fulfillmentCenterId: currentUser.organization.id,
|
||
status: 'SHIPPED',
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true, // Селлер-создатель заказа
|
||
partner: true, // Поставщик
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для приема',
|
||
}
|
||
}
|
||
|
||
// Обновляем статус заказа
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'DELIVERED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// 🔄 СИНХРОНИЗАЦИЯ СКЛАДА ПОСТАВЩИКА: Обновляем остатки поставщика согласно правилам
|
||
console.warn('🔄 Начинаем синхронизацию остатков поставщика...')
|
||
for (const item of existingOrder.items) {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: item.product.id },
|
||
})
|
||
|
||
if (product) {
|
||
// ИСПРАВЛЕНО: НЕ списываем повторно, только переводим из inTransit в sold
|
||
// Остаток уже был уменьшен при создании/одобрении заказа
|
||
await prisma.product.update({
|
||
where: { id: item.product.id },
|
||
data: {
|
||
// НЕ ТРОГАЕМ stock - он уже правильно уменьшен при заказе
|
||
// Только переводим из inTransit в sold
|
||
inTransit: Math.max((product.inTransit || 0) - item.quantity, 0),
|
||
sold: (product.sold || 0) + item.quantity,
|
||
},
|
||
})
|
||
console.warn(`✅ Товар поставщика "${product.name}" обновлен: получено ${item.quantity} единиц`)
|
||
console.warn(
|
||
` 📊 Остаток: ${product.stock || product.quantity || 0} (НЕ ИЗМЕНЕН - уже списан при заказе)`,
|
||
)
|
||
console.warn(
|
||
` 🚚 В пути: ${product.inTransit || 0} -> ${Math.max(
|
||
(product.inTransit || 0) - item.quantity,
|
||
0,
|
||
)} (УБЫЛО: ${item.quantity})`,
|
||
)
|
||
console.warn(
|
||
` 💰 Продано: ${product.sold || 0} -> ${
|
||
(product.sold || 0) + item.quantity
|
||
} (ПРИБЫЛО: ${item.quantity})`,
|
||
)
|
||
}
|
||
}
|
||
|
||
// Обновляем склад фулфилмента с учетом типа расходников
|
||
console.warn('📦 Обновляем склад фулфилмента...')
|
||
console.warn(`🏷️ Тип поставки: ${existingOrder.consumableType || 'FULFILLMENT_CONSUMABLES'}`)
|
||
|
||
for (const item of existingOrder.items) {
|
||
// Определяем тип расходников и владельца
|
||
const isSellerSupply = existingOrder.consumableType === 'SELLER_CONSUMABLES'
|
||
const supplyType = isSellerSupply ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES'
|
||
const sellerOwnerId = isSellerSupply ? updatedOrder.organization?.id : null
|
||
|
||
// Для расходников селлеров ищем по имени И по владельцу
|
||
const whereCondition = isSellerSupply
|
||
? {
|
||
organizationId: currentUser.organization.id,
|
||
name: item.product.name,
|
||
type: 'SELLER_CONSUMABLES' as const,
|
||
sellerOwnerId: sellerOwnerId,
|
||
}
|
||
: {
|
||
organizationId: currentUser.organization.id,
|
||
name: item.product.name,
|
||
type: 'FULFILLMENT_CONSUMABLES' as const,
|
||
}
|
||
|
||
const existingSupply = await prisma.supply.findFirst({
|
||
where: whereCondition,
|
||
})
|
||
|
||
if (existingSupply) {
|
||
await prisma.supply.update({
|
||
where: { id: existingSupply.id },
|
||
data: {
|
||
currentStock: existingSupply.currentStock + item.quantity,
|
||
quantity: existingSupply.quantity + item.quantity,
|
||
status: 'in-stock',
|
||
},
|
||
})
|
||
console.warn(
|
||
`📈 Обновлен существующий ${
|
||
isSellerSupply ? 'расходник селлера' : 'расходник фулфилмента'
|
||
} "${item.product.name}" ${
|
||
isSellerSupply ? `(владелец: ${updatedOrder.organization?.name})` : ''
|
||
}: ${existingSupply.currentStock} -> ${existingSupply.currentStock + item.quantity}`,
|
||
)
|
||
} else {
|
||
await prisma.supply.create({
|
||
data: {
|
||
name: item.product.name,
|
||
description: isSellerSupply
|
||
? `Расходники селлера ${updatedOrder.organization?.name || updatedOrder.organization?.fullName}`
|
||
: item.product.description || `Расходники от ${updatedOrder.partner.name}`,
|
||
price: item.price, // Цена закупки у поставщика
|
||
quantity: item.quantity,
|
||
currentStock: item.quantity,
|
||
usedStock: 0,
|
||
unit: 'шт',
|
||
category: item.product.category?.name || 'Расходники',
|
||
status: 'in-stock',
|
||
supplier: updatedOrder.partner.name || updatedOrder.partner.fullName || 'Поставщик',
|
||
type: supplyType as 'SELLER_CONSUMABLES' | 'FULFILLMENT_CONSUMABLES',
|
||
sellerOwnerId: sellerOwnerId,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
console.warn(
|
||
`➕ Создан новый ${
|
||
isSellerSupply ? 'расходник селлера' : 'расходник фулфилмента'
|
||
} "${item.product.name}" ${
|
||
isSellerSupply ? `(владелец: ${updatedOrder.organization?.name})` : ''
|
||
}: ${item.quantity} единиц`,
|
||
)
|
||
}
|
||
}
|
||
|
||
console.warn('🎉 Синхронизация склада завершена успешно!')
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Заказ принят фулфилментом. Склад обновлен. Остатки поставщика синхронизированы.',
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error receiving supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при приеме заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
updateExternalAdClicks: async (_: unknown, { id, clicks }: { id: string; clicks: number }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Проверяем, что реклама принадлежит организации пользователя
|
||
const existingAd = await prisma.externalAd.findFirst({
|
||
where: {
|
||
id,
|
||
organizationId: user.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingAd) {
|
||
throw new GraphQLError('Внешняя реклама не найдена')
|
||
}
|
||
|
||
await prisma.externalAd.update({
|
||
where: { id },
|
||
data: { clicks },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Клики успешно обновлены',
|
||
externalAd: null,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating external ad clicks:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка обновления кликов',
|
||
externalAd: null,
|
||
}
|
||
}
|
||
},
|
||
},
|
||
|
||
// Резолверы типов
|
||
Organization: {
|
||
users: async (parent: { id: string; users?: unknown[] }) => {
|
||
// Если пользователи уже загружены через include, возвращаем их
|
||
if (parent.users) {
|
||
return parent.users
|
||
}
|
||
|
||
// Иначе загружаем отдельно
|
||
return await prisma.user.findMany({
|
||
where: { organizationId: parent.id },
|
||
})
|
||
},
|
||
services: async (parent: { id: string; services?: unknown[] }) => {
|
||
// Если услуги уже загружены через include, возвращаем их
|
||
if (parent.services) {
|
||
return parent.services
|
||
}
|
||
|
||
// Иначе загружаем отдельно
|
||
return await prisma.service.findMany({
|
||
where: { organizationId: parent.id },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
supplies: async (parent: { id: string; supplies?: unknown[] }) => {
|
||
// Если расходники уже загружены через include, возвращаем их
|
||
if (parent.supplies) {
|
||
return parent.supplies
|
||
}
|
||
|
||
// Иначе загружаем отдельно
|
||
return await prisma.supply.findMany({
|
||
where: { organizationId: parent.id },
|
||
include: {
|
||
organization: true,
|
||
sellerOwner: true, // Включаем информацию о селлере-владельце
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
},
|
||
|
||
Cart: {
|
||
totalPrice: (parent: { items: Array<{ product: { price: number }; quantity: number }> }) => {
|
||
return parent.items.reduce((total, item) => {
|
||
return total + Number(item.product.price) * item.quantity
|
||
}, 0)
|
||
},
|
||
totalItems: (parent: { items: Array<{ quantity: number }> }) => {
|
||
return parent.items.reduce((total, item) => total + item.quantity, 0)
|
||
},
|
||
},
|
||
|
||
CartItem: {
|
||
totalPrice: (parent: { product: { price: number }; quantity: number }) => {
|
||
return Number(parent.product.price) * parent.quantity
|
||
},
|
||
isAvailable: (parent: { product: { quantity: number; isActive: boolean }; quantity: number }) => {
|
||
return parent.product.isActive && parent.product.quantity >= parent.quantity
|
||
},
|
||
availableQuantity: (parent: { product: { quantity: number } }) => {
|
||
return parent.product.quantity
|
||
},
|
||
},
|
||
|
||
User: {
|
||
organization: async (parent: { organizationId?: string; organization?: unknown }) => {
|
||
// Если организация уже загружена через include, возвращаем её
|
||
if (parent.organization) {
|
||
return parent.organization
|
||
}
|
||
|
||
// Иначе загружаем отдельно если есть organizationId
|
||
if (parent.organizationId) {
|
||
return await prisma.organization.findUnique({
|
||
where: { id: parent.organizationId },
|
||
include: {
|
||
apiKeys: true,
|
||
users: true,
|
||
},
|
||
})
|
||
}
|
||
|
||
return null
|
||
},
|
||
},
|
||
|
||
Product: {
|
||
type: (parent: { type?: string | null }) => parent.type || 'PRODUCT',
|
||
images: (parent: { images: unknown }) => {
|
||
// Если images это строка JSON, парсим её в массив
|
||
if (typeof parent.images === 'string') {
|
||
try {
|
||
return JSON.parse(parent.images)
|
||
} catch {
|
||
return []
|
||
}
|
||
}
|
||
// Если это уже массив, возвращаем как есть
|
||
if (Array.isArray(parent.images)) {
|
||
return parent.images
|
||
}
|
||
// Иначе возвращаем пустой массив
|
||
return []
|
||
},
|
||
},
|
||
|
||
Message: {
|
||
type: (parent: { type?: string | null }) => {
|
||
return parent.type || 'TEXT'
|
||
},
|
||
createdAt: (parent: { createdAt: Date | string }) => {
|
||
if (parent.createdAt instanceof Date) {
|
||
return parent.createdAt.toISOString()
|
||
}
|
||
return parent.createdAt
|
||
},
|
||
updatedAt: (parent: { updatedAt: Date | string }) => {
|
||
if (parent.updatedAt instanceof Date) {
|
||
return parent.updatedAt.toISOString()
|
||
}
|
||
return parent.updatedAt
|
||
},
|
||
},
|
||
|
||
Employee: {
|
||
fullName: (parent: { firstName: string; lastName: string; middleName?: string }) => {
|
||
const parts = [parent.lastName, parent.firstName]
|
||
if (parent.middleName) {
|
||
parts.push(parent.middleName)
|
||
}
|
||
return parts.join(' ')
|
||
},
|
||
name: (parent: { firstName: string; lastName: string }) => {
|
||
return `${parent.firstName} ${parent.lastName}`
|
||
},
|
||
birthDate: (parent: { birthDate?: Date | string | null }) => {
|
||
if (!parent.birthDate) return null
|
||
if (parent.birthDate instanceof Date) {
|
||
return parent.birthDate.toISOString()
|
||
}
|
||
return parent.birthDate
|
||
},
|
||
passportDate: (parent: { passportDate?: Date | string | null }) => {
|
||
if (!parent.passportDate) return null
|
||
if (parent.passportDate instanceof Date) {
|
||
return parent.passportDate.toISOString()
|
||
}
|
||
return parent.passportDate
|
||
},
|
||
hireDate: (parent: { hireDate: Date | string }) => {
|
||
if (parent.hireDate instanceof Date) {
|
||
return parent.hireDate.toISOString()
|
||
}
|
||
return parent.hireDate
|
||
},
|
||
createdAt: (parent: { createdAt: Date | string }) => {
|
||
if (parent.createdAt instanceof Date) {
|
||
return parent.createdAt.toISOString()
|
||
}
|
||
return parent.createdAt
|
||
},
|
||
updatedAt: (parent: { updatedAt: Date | string }) => {
|
||
if (parent.updatedAt instanceof Date) {
|
||
return parent.updatedAt.toISOString()
|
||
}
|
||
return parent.updatedAt
|
||
},
|
||
},
|
||
|
||
EmployeeSchedule: {
|
||
date: (parent: { date: Date | string }) => {
|
||
if (parent.date instanceof Date) {
|
||
return parent.date.toISOString()
|
||
}
|
||
return parent.date
|
||
},
|
||
createdAt: (parent: { createdAt: Date | string }) => {
|
||
if (parent.createdAt instanceof Date) {
|
||
return parent.createdAt.toISOString()
|
||
}
|
||
return parent.createdAt
|
||
},
|
||
updatedAt: (parent: { updatedAt: Date | string }) => {
|
||
if (parent.updatedAt instanceof Date) {
|
||
return parent.updatedAt.toISOString()
|
||
}
|
||
return parent.updatedAt
|
||
},
|
||
employee: async (parent: { employeeId: string }) => {
|
||
return await prisma.employee.findUnique({
|
||
where: { id: parent.employeeId },
|
||
})
|
||
},
|
||
},
|
||
}
|
||
|
||
// Мутации для категорий
|
||
const categoriesMutations = {
|
||
// Создать категорию
|
||
createCategory: async (_: unknown, args: { input: { name: string } }) => {
|
||
try {
|
||
// Проверяем есть ли уже категория с таким именем
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { name: args.input.name },
|
||
})
|
||
|
||
if (existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория с таким названием уже существует',
|
||
}
|
||
}
|
||
|
||
const category = await prisma.category.create({
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Категория успешно создана',
|
||
category,
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка создания категории:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании категории',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить категорию
|
||
updateCategory: async (_: unknown, args: { id: string; input: { name: string } }) => {
|
||
try {
|
||
// Проверяем существует ли категория
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
if (!existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория не найдена',
|
||
}
|
||
}
|
||
|
||
// Проверяем не занято ли имя другой категорией
|
||
const duplicateCategory = await prisma.category.findFirst({
|
||
where: {
|
||
name: args.input.name,
|
||
id: { not: args.id },
|
||
},
|
||
})
|
||
|
||
if (duplicateCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория с таким названием уже существует',
|
||
}
|
||
}
|
||
|
||
const category = await prisma.category.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Категория успешно обновлена',
|
||
category,
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка обновления категории:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении категории',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить категорию
|
||
deleteCategory: async (_: unknown, args: { id: string }) => {
|
||
try {
|
||
// Проверяем существует ли категория
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
if (!existingCategory) {
|
||
throw new GraphQLError('Категория не найдена')
|
||
}
|
||
|
||
// Проверяем есть ли товары в этой категории
|
||
const productsCount = await prisma.product.count({
|
||
where: { categoryId: args.id },
|
||
})
|
||
|
||
if (productsCount > 0) {
|
||
throw new GraphQLError('Нельзя удалить категорию, в которой есть товары')
|
||
}
|
||
|
||
await prisma.category.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Ошибка удаления категории:', error)
|
||
if (error instanceof GraphQLError) {
|
||
throw error
|
||
}
|
||
throw new GraphQLError('Ошибка при удалении категории')
|
||
}
|
||
},
|
||
}
|
||
|
||
// Логистические мутации
|
||
const logisticsMutations = {
|
||
// Создать логистический маршрут
|
||
createLogistics: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
fromLocation: string
|
||
toLocation: string
|
||
priceUnder1m3: number
|
||
priceOver1m3: number
|
||
description?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const logistics = await prisma.logistics.create({
|
||
data: {
|
||
fromLocation: args.input.fromLocation,
|
||
toLocation: args.input.toLocation,
|
||
priceUnder1m3: args.input.priceUnder1m3,
|
||
priceOver1m3: args.input.priceOver1m3,
|
||
description: args.input.description,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
console.warn('✅ Logistics created:', logistics.id)
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Логистический маршрут создан',
|
||
logistics,
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error creating logistics:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании логистического маршрута',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить логистический маршрут
|
||
updateLogistics: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
input: {
|
||
fromLocation: string
|
||
toLocation: string
|
||
priceUnder1m3: number
|
||
priceOver1m3: number
|
||
description?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что маршрут принадлежит организации пользователя
|
||
const existingLogistics = await prisma.logistics.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingLogistics) {
|
||
throw new GraphQLError('Логистический маршрут не найден')
|
||
}
|
||
|
||
const logistics = await prisma.logistics.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
fromLocation: args.input.fromLocation,
|
||
toLocation: args.input.toLocation,
|
||
priceUnder1m3: args.input.priceUnder1m3,
|
||
priceOver1m3: args.input.priceOver1m3,
|
||
description: args.input.description,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
console.warn('✅ Logistics updated:', logistics.id)
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Логистический маршрут обновлен',
|
||
logistics,
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error updating logistics:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении логистического маршрута',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить логистический маршрут
|
||
deleteLogistics: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что маршрут принадлежит организации пользователя
|
||
const existingLogistics = await prisma.logistics.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingLogistics) {
|
||
throw new GraphQLError('Логистический маршрут не найден')
|
||
}
|
||
|
||
await prisma.logistics.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
console.warn('✅ Logistics deleted:', args.id)
|
||
return true
|
||
} catch (error) {
|
||
console.error('❌ Error deleting logistics:', error)
|
||
return false
|
||
}
|
||
},
|
||
}
|
||
|
||
// Добавляем дополнительные мутации к основным резолверам
|
||
resolvers.Mutation = {
|
||
...resolvers.Mutation,
|
||
...categoriesMutations,
|
||
...logisticsMutations,
|
||
}
|
||
|
||
// Админ резолверы
|
||
const adminQueries = {
|
||
adminMe: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.admin) {
|
||
throw new GraphQLError('Требуется авторизация администратора', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const admin = await prisma.admin.findUnique({
|
||
where: { id: context.admin.id },
|
||
})
|
||
|
||
if (!admin) {
|
||
throw new GraphQLError('Администратор не найден')
|
||
}
|
||
|
||
return admin
|
||
},
|
||
|
||
allUsers: async (_: unknown, args: { search?: string; limit?: number; offset?: number }, context: Context) => {
|
||
if (!context.admin) {
|
||
throw new GraphQLError('Требуется авторизация администратора', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const limit = args.limit || 50
|
||
const offset = args.offset || 0
|
||
|
||
// Строим условие поиска
|
||
const whereCondition: Prisma.UserWhereInput = args.search
|
||
? {
|
||
OR: [
|
||
{ phone: { contains: args.search, mode: 'insensitive' } },
|
||
{ managerName: { contains: args.search, mode: 'insensitive' } },
|
||
{
|
||
organization: {
|
||
OR: [
|
||
{ name: { contains: args.search, mode: 'insensitive' } },
|
||
{ fullName: { contains: args.search, mode: 'insensitive' } },
|
||
{ inn: { contains: args.search, mode: 'insensitive' } },
|
||
],
|
||
},
|
||
},
|
||
],
|
||
}
|
||
: {}
|
||
|
||
// Получаем пользователей с пагинацией
|
||
const [users, total] = await Promise.all([
|
||
prisma.user.findMany({
|
||
where: whereCondition,
|
||
include: {
|
||
organization: true,
|
||
},
|
||
take: limit,
|
||
skip: offset,
|
||
orderBy: { createdAt: 'desc' },
|
||
}),
|
||
prisma.user.count({
|
||
where: whereCondition,
|
||
}),
|
||
])
|
||
|
||
return {
|
||
users,
|
||
total,
|
||
hasMore: offset + limit < total,
|
||
}
|
||
},
|
||
}
|
||
|
||
const adminMutations = {
|
||
adminLogin: async (_: unknown, args: { username: string; password: string }) => {
|
||
try {
|
||
// Найти администратора
|
||
const admin = await prisma.admin.findUnique({
|
||
where: { username: args.username },
|
||
})
|
||
|
||
if (!admin) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверные учетные данные',
|
||
}
|
||
}
|
||
|
||
// Проверить активность
|
||
if (!admin.isActive) {
|
||
return {
|
||
success: false,
|
||
message: 'Аккаунт заблокирован',
|
||
}
|
||
}
|
||
|
||
// Проверить пароль
|
||
const isPasswordValid = await bcrypt.compare(args.password, admin.password)
|
||
|
||
if (!isPasswordValid) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверные учетные данные',
|
||
}
|
||
}
|
||
|
||
// Обновить время последнего входа
|
||
await prisma.admin.update({
|
||
where: { id: admin.id },
|
||
data: { lastLogin: new Date() },
|
||
})
|
||
|
||
// Создать токен
|
||
const token = jwt.sign(
|
||
{
|
||
adminId: admin.id,
|
||
username: admin.username,
|
||
type: 'admin',
|
||
},
|
||
process.env.JWT_SECRET!,
|
||
{ expiresIn: '24h' },
|
||
)
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Успешная авторизация',
|
||
token,
|
||
admin: {
|
||
...admin,
|
||
password: undefined, // Не возвращаем пароль
|
||
},
|
||
}
|
||
} catch (error) {
|
||
console.error('Admin login error:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка авторизации',
|
||
}
|
||
}
|
||
},
|
||
|
||
adminLogout: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.admin) {
|
||
throw new GraphQLError('Требуется авторизация администратора', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
return true
|
||
},
|
||
}
|
||
|
||
// Wildberries статистика
|
||
const wildberriesQueries = {
|
||
debugWildberriesAdverts: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization || user.organization.type !== 'SELLER') {
|
||
throw new GraphQLError('Доступно только для продавцов')
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError('WB API ключ не настроен')
|
||
}
|
||
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
|
||
|
||
// Получаем кампании во всех статусах
|
||
const [active, completed, paused] = await Promise.all([
|
||
wbService.getAdverts(9).catch(() => []), // активные
|
||
wbService.getAdverts(7).catch(() => []), // завершенные
|
||
wbService.getAdverts(11).catch(() => []), // на паузе
|
||
])
|
||
|
||
const allCampaigns = [...active, ...completed, ...paused]
|
||
|
||
return {
|
||
success: true,
|
||
message: `Found ${active.length} active, ${completed.length} completed, ${paused.length} paused campaigns`,
|
||
campaignsCount: allCampaigns.length,
|
||
campaigns: allCampaigns.map((c) => ({
|
||
id: c.advertId,
|
||
name: c.name,
|
||
status: c.status,
|
||
type: c.type,
|
||
})),
|
||
}
|
||
} catch (error) {
|
||
console.error('Error debugging WB adverts:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Unknown error',
|
||
campaignsCount: 0,
|
||
campaigns: [],
|
||
}
|
||
}
|
||
},
|
||
|
||
getWildberriesStatistics: async (
|
||
_: unknown,
|
||
{
|
||
period,
|
||
startDate,
|
||
endDate,
|
||
}: {
|
||
period?: 'week' | 'month' | 'quarter'
|
||
startDate?: string
|
||
endDate?: string
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Получаем организацию пользователя и её WB API ключ
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
if (user.organization.type !== 'SELLER') {
|
||
throw new GraphQLError('Доступно только для продавцов')
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError('WB API ключ не настроен')
|
||
}
|
||
|
||
// Создаем экземпляр сервиса
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
|
||
|
||
// Получаем даты
|
||
let dateFrom: string
|
||
let dateTo: string
|
||
|
||
if (startDate && endDate) {
|
||
// Используем пользовательские даты
|
||
dateFrom = startDate
|
||
dateTo = endDate
|
||
} else if (period) {
|
||
// Используем предустановленный период
|
||
dateFrom = WildberriesService.getDatePeriodAgo(period)
|
||
dateTo = WildberriesService.formatDate(new Date())
|
||
} else {
|
||
throw new GraphQLError('Необходимо указать либо period, либо startDate и endDate')
|
||
}
|
||
|
||
// Получаем статистику
|
||
const statistics = await wbService.getStatistics(dateFrom, dateTo)
|
||
|
||
return {
|
||
success: true,
|
||
data: statistics,
|
||
message: null,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching WB statistics:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения статистики',
|
||
data: [],
|
||
}
|
||
}
|
||
},
|
||
|
||
getWildberriesCampaignStats: async (
|
||
_: unknown,
|
||
{
|
||
input,
|
||
}: {
|
||
input: {
|
||
campaigns: Array<{
|
||
id: number
|
||
dates?: string[]
|
||
interval?: {
|
||
begin: string
|
||
end: string
|
||
}
|
||
}>
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Получаем организацию пользователя и её WB API ключ
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
if (user.organization.type !== 'SELLER') {
|
||
throw new GraphQLError('Доступно только для продавцов')
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError('WB API ключ не настроен')
|
||
}
|
||
|
||
// Создаем экземпляр сервиса
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
|
||
|
||
// Преобразуем запросы в нужный формат
|
||
const requests = input.campaigns.map((campaign) => {
|
||
if (campaign.dates && campaign.dates.length > 0) {
|
||
return {
|
||
id: campaign.id,
|
||
dates: campaign.dates,
|
||
}
|
||
} else if (campaign.interval) {
|
||
return {
|
||
id: campaign.id,
|
||
interval: campaign.interval,
|
||
}
|
||
} else {
|
||
// Если не указаны ни даты, ни интервал, возвращаем данные только за последние сутки
|
||
return {
|
||
id: campaign.id,
|
||
}
|
||
}
|
||
})
|
||
|
||
// Получаем статистику кампаний
|
||
const campaignStats = await wbService.getCampaignStats(requests)
|
||
|
||
return {
|
||
success: true,
|
||
data: campaignStats,
|
||
message: null,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching WB campaign stats:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения статистики кампаний',
|
||
data: [],
|
||
}
|
||
}
|
||
},
|
||
|
||
getWildberriesCampaignsList: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Получаем организацию пользователя и её WB API ключ
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
if (user.organization.type !== 'SELLER') {
|
||
throw new GraphQLError('Доступно только для продавцов')
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError('WB API ключ не настроен')
|
||
}
|
||
|
||
// Создаем экземпляр сервиса
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
|
||
|
||
// Получаем список кампаний
|
||
const campaignsList = await wbService.getCampaignsList()
|
||
|
||
return {
|
||
success: true,
|
||
data: campaignsList,
|
||
message: null,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching WB campaigns list:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения списка кампаний',
|
||
data: {
|
||
adverts: [],
|
||
all: 0,
|
||
},
|
||
}
|
||
}
|
||
},
|
||
|
||
// Получение заявок покупателей на возврат от Wildberries от всех партнеров-селлеров
|
||
wbReturnClaims: async (
|
||
_: unknown,
|
||
{ isArchive, limit, offset }: { isArchive: boolean; limit?: number; offset?: number },
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Получаем текущую организацию пользователя (фулфилмент)
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент организация
|
||
if (user.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступ только для фулфилмент организаций')
|
||
}
|
||
|
||
// Получаем всех партнеров-селлеров с активными WB API ключами
|
||
const partnerSellerOrgs = await prisma.counterparty.findMany({
|
||
where: {
|
||
organizationId: user.organization.id,
|
||
},
|
||
include: {
|
||
counterparty: {
|
||
include: {
|
||
apiKeys: {
|
||
where: {
|
||
marketplace: 'WILDBERRIES',
|
||
isActive: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// Фильтруем только селлеров с WB API ключами
|
||
const sellersWithWbKeys = partnerSellerOrgs.filter(
|
||
(partner) => partner.counterparty.type === 'SELLER' && partner.counterparty.apiKeys.length > 0,
|
||
)
|
||
|
||
if (sellersWithWbKeys.length === 0) {
|
||
return {
|
||
claims: [],
|
||
total: 0,
|
||
}
|
||
}
|
||
|
||
console.warn(`Found ${sellersWithWbKeys.length} seller partners with WB keys`)
|
||
|
||
// Получаем заявки от всех селлеров параллельно
|
||
const claimsPromises = sellersWithWbKeys.map(async (partner) => {
|
||
const wbApiKey = partner.counterparty.apiKeys[0].apiKey
|
||
const wbService = new WildberriesService(wbApiKey)
|
||
|
||
try {
|
||
const claimsResponse = await wbService.getClaims({
|
||
isArchive,
|
||
limit: Math.ceil((limit || 50) / sellersWithWbKeys.length), // Распределяем лимит между селлерами
|
||
offset: 0,
|
||
})
|
||
|
||
// Добавляем информацию о селлере к каждой заявке
|
||
const claimsWithSeller = claimsResponse.claims.map((claim) => ({
|
||
...claim,
|
||
sellerOrganization: {
|
||
id: partner.counterparty.id,
|
||
name: partner.counterparty.name || 'Неизвестная организация',
|
||
inn: partner.counterparty.inn || '',
|
||
},
|
||
}))
|
||
|
||
console.warn(`Got ${claimsWithSeller.length} claims from seller ${partner.counterparty.name}`)
|
||
return claimsWithSeller
|
||
} catch (error) {
|
||
console.error(`Error fetching claims for seller ${partner.counterparty.name}:`, error)
|
||
return []
|
||
}
|
||
})
|
||
|
||
const allClaims = (await Promise.all(claimsPromises)).flat()
|
||
console.warn(`Total claims aggregated: ${allClaims.length}`)
|
||
|
||
// Сортируем по дате создания (новые первыми)
|
||
allClaims.sort((a, b) => new Date(b.dt).getTime() - new Date(a.dt).getTime())
|
||
|
||
// Применяем пагинацию
|
||
const paginatedClaims = allClaims.slice(offset || 0, (offset || 0) + (limit || 50))
|
||
console.warn(`Paginated claims: ${paginatedClaims.length}`)
|
||
|
||
// Преобразуем в формат фронтенда
|
||
const transformedClaims = paginatedClaims.map((claim) => ({
|
||
id: claim.id,
|
||
claimType: claim.claim_type,
|
||
status: claim.status,
|
||
statusEx: claim.status_ex,
|
||
nmId: claim.nm_id,
|
||
userComment: claim.user_comment || '',
|
||
wbComment: claim.wb_comment || null,
|
||
dt: claim.dt,
|
||
imtName: claim.imt_name,
|
||
orderDt: claim.order_dt,
|
||
dtUpdate: claim.dt_update,
|
||
photos: claim.photos || [],
|
||
videoPaths: claim.video_paths || [],
|
||
actions: claim.actions || [],
|
||
price: claim.price,
|
||
currencyCode: claim.currency_code,
|
||
srid: claim.srid,
|
||
sellerOrganization: claim.sellerOrganization,
|
||
}))
|
||
|
||
console.warn(`Returning ${transformedClaims.length} transformed claims to frontend`)
|
||
|
||
return {
|
||
claims: transformedClaims,
|
||
total: allClaims.length,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching WB return claims:', error)
|
||
throw new GraphQLError(error instanceof Error ? error.message : 'Ошибка получения заявок на возврат')
|
||
}
|
||
},
|
||
}
|
||
|
||
// Резолверы для внешней рекламы
|
||
const externalAdQueries = {
|
||
getExternalAds: async (_: unknown, { dateFrom, dateTo }: { dateFrom: string; dateTo: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
const externalAds = await prisma.externalAd.findMany({
|
||
where: {
|
||
organizationId: user.organization.id,
|
||
date: {
|
||
gte: new Date(dateFrom),
|
||
lte: new Date(dateTo + 'T23:59:59.999Z'),
|
||
},
|
||
},
|
||
orderBy: {
|
||
date: 'desc',
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: null,
|
||
externalAds: externalAds.map((ad) => ({
|
||
...ad,
|
||
cost: parseFloat(ad.cost.toString()),
|
||
date: ad.date.toISOString().split('T')[0],
|
||
createdAt: ad.createdAt.toISOString(),
|
||
updatedAt: ad.updatedAt.toISOString(),
|
||
})),
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching external ads:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения внешней рекламы',
|
||
externalAds: [],
|
||
}
|
||
}
|
||
},
|
||
}
|
||
|
||
const externalAdMutations = {
|
||
createExternalAd: async (
|
||
_: unknown,
|
||
{
|
||
input,
|
||
}: {
|
||
input: {
|
||
name: string
|
||
url: string
|
||
cost: number
|
||
date: string
|
||
nmId: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
const externalAd = await prisma.externalAd.create({
|
||
data: {
|
||
name: input.name,
|
||
url: input.url,
|
||
cost: input.cost,
|
||
date: new Date(input.date),
|
||
nmId: input.nmId,
|
||
organizationId: user.organization.id,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Внешняя реклама успешно создана',
|
||
externalAd: {
|
||
...externalAd,
|
||
cost: parseFloat(externalAd.cost.toString()),
|
||
date: externalAd.date.toISOString().split('T')[0],
|
||
createdAt: externalAd.createdAt.toISOString(),
|
||
updatedAt: externalAd.updatedAt.toISOString(),
|
||
},
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating external ad:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка создания внешней рекламы',
|
||
externalAd: null,
|
||
}
|
||
}
|
||
},
|
||
|
||
updateExternalAd: async (
|
||
_: unknown,
|
||
{
|
||
id,
|
||
input,
|
||
}: {
|
||
id: string
|
||
input: {
|
||
name: string
|
||
url: string
|
||
cost: number
|
||
date: string
|
||
nmId: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Проверяем, что реклама принадлежит организации пользователя
|
||
const existingAd = await prisma.externalAd.findFirst({
|
||
where: {
|
||
id,
|
||
organizationId: user.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingAd) {
|
||
throw new GraphQLError('Внешняя реклама не найдена')
|
||
}
|
||
|
||
const externalAd = await prisma.externalAd.update({
|
||
where: { id },
|
||
data: {
|
||
name: input.name,
|
||
url: input.url,
|
||
cost: input.cost,
|
||
date: new Date(input.date),
|
||
nmId: input.nmId,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Внешняя реклама успешно обновлена',
|
||
externalAd: {
|
||
...externalAd,
|
||
cost: parseFloat(externalAd.cost.toString()),
|
||
date: externalAd.date.toISOString().split('T')[0],
|
||
createdAt: externalAd.createdAt.toISOString(),
|
||
updatedAt: externalAd.updatedAt.toISOString(),
|
||
},
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating external ad:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка обновления внешней рекламы',
|
||
externalAd: null,
|
||
}
|
||
}
|
||
},
|
||
|
||
deleteExternalAd: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Проверяем, что реклама принадлежит организации пользователя
|
||
const existingAd = await prisma.externalAd.findFirst({
|
||
where: {
|
||
id,
|
||
organizationId: user.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingAd) {
|
||
throw new GraphQLError('Внешняя реклама не найдена')
|
||
}
|
||
|
||
await prisma.externalAd.delete({
|
||
where: { id },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Внешняя реклама успешно удалена',
|
||
externalAd: null,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting external ad:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка удаления внешней рекламы',
|
||
externalAd: null,
|
||
}
|
||
}
|
||
},
|
||
}
|
||
|
||
// Резолверы для кеша склада WB
|
||
const wbWarehouseCacheQueries = {
|
||
getWBWarehouseData: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Получаем текущую дату без времени
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
|
||
// Ищем кеш за сегодня
|
||
const cache = await prisma.wBWarehouseCache.findFirst({
|
||
where: {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
},
|
||
orderBy: {
|
||
createdAt: 'desc',
|
||
},
|
||
})
|
||
|
||
if (cache) {
|
||
// Возвращаем данные из кеша
|
||
return {
|
||
success: true,
|
||
message: 'Данные получены из кеша',
|
||
cache: {
|
||
...cache,
|
||
cacheDate: cache.cacheDate.toISOString().split('T')[0],
|
||
createdAt: cache.createdAt.toISOString(),
|
||
updatedAt: cache.updatedAt.toISOString(),
|
||
},
|
||
fromCache: true,
|
||
}
|
||
} else {
|
||
// Кеша нет, нужно загрузить данные из API
|
||
return {
|
||
success: true,
|
||
message: 'Кеш не найден, требуется загрузка из API',
|
||
cache: null,
|
||
fromCache: false,
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error getting WB warehouse cache:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения кеша склада WB',
|
||
cache: null,
|
||
fromCache: false,
|
||
}
|
||
}
|
||
},
|
||
}
|
||
|
||
const wbWarehouseCacheMutations = {
|
||
saveWBWarehouseCache: async (
|
||
_: unknown,
|
||
{
|
||
input,
|
||
}: {
|
||
input: {
|
||
data: string
|
||
totalProducts: number
|
||
totalStocks: number
|
||
totalReserved: number
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Получаем текущую дату без времени
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
|
||
// Используем upsert для создания или обновления кеша
|
||
const cache = await prisma.wBWarehouseCache.upsert({
|
||
where: {
|
||
organizationId_cacheDate: {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
},
|
||
},
|
||
update: {
|
||
data: input.data,
|
||
totalProducts: input.totalProducts,
|
||
totalStocks: input.totalStocks,
|
||
totalReserved: input.totalReserved,
|
||
},
|
||
create: {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
data: input.data,
|
||
totalProducts: input.totalProducts,
|
||
totalStocks: input.totalStocks,
|
||
totalReserved: input.totalReserved,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Кеш склада WB успешно сохранен',
|
||
cache: {
|
||
...cache,
|
||
cacheDate: cache.cacheDate.toISOString().split('T')[0],
|
||
createdAt: cache.createdAt.toISOString(),
|
||
updatedAt: cache.updatedAt.toISOString(),
|
||
},
|
||
fromCache: false,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving WB warehouse cache:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка сохранения кеша склада WB',
|
||
cache: null,
|
||
fromCache: false,
|
||
}
|
||
}
|
||
},
|
||
}
|
||
|
||
// Добавляем админ запросы и мутации к основным резолверам
|
||
resolvers.Query = {
|
||
...resolvers.Query,
|
||
...adminQueries,
|
||
...wildberriesQueries,
|
||
...externalAdQueries,
|
||
...wbWarehouseCacheQueries,
|
||
}
|
||
|
||
resolvers.Mutation = {
|
||
...resolvers.Mutation,
|
||
...adminMutations,
|
||
...externalAdMutations,
|
||
...wbWarehouseCacheMutations,
|
||
}
|