
- Обновлена форма создания поставок расходников фулфилмента для использования v2 GraphQL API - Заменена мутация CREATE_SUPPLY_ORDER на CREATE_FULFILLMENT_CONSUMABLE_SUPPLY - Обновлена структура input данных под новый формат v2 - Сделано поле логистики опциональным - Добавлено поле notes для комментариев к поставке - Обновлены refetchQueries на новые v2 запросы - Исправлены TypeScript ошибки в интерфейсах - Удалена дублирующая страница consumables-v2 - Сохранен оригинальный богатый UI интерфейс формы (819 строк) - Подтверждена работа с новой таблицей FulfillmentConsumableSupplyOrder Технические изменения: - src/components/fulfillment-supplies/create-fulfillment-consumables-supply-v2.tsx - основная форма - src/components/fulfillment-supplies/fulfillment-supplies-layout.tsx - обновлена навигация - Добавлены недостающие поля quantity и ordered в интерфейсы продуктов - Исправлены импорты и зависимости Результат: форма полностью интегрирована с v2 системой поставок, которая использует отдельные таблицы для каждого типа поставок согласно новой архитектуре. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
10268 lines
356 KiB
TypeScript
10268 lines
356 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 { notifyMany, notifyOrganization } from '@/lib/realtime'
|
||
import { DaDataService } from '@/services/dadata-service'
|
||
import { MarketplaceService } from '@/services/marketplace-service'
|
||
import { SmsService } from '@/services/sms-service'
|
||
import { WildberriesService } from '@/services/wildberries-service'
|
||
|
||
import '@/lib/seed-init' // Автоматическая инициализация БД
|
||
|
||
// Импорт новых resolvers для системы поставок v2
|
||
import { fulfillmentConsumableV2Queries, fulfillmentConsumableV2Mutations } from './resolvers/fulfillment-consumables-v2'
|
||
|
||
// 🔒 СИСТЕМА БЕЗОПАСНОСТИ - импорты
|
||
import { CommercialDataAudit } from './security/commercial-data-audit'
|
||
import { createSecurityContext } from './security/index'
|
||
|
||
// 🔒 HELPER: Создание безопасного контекста с организационными данными
|
||
function createSecureContextWithOrgData(context: Context, currentUser: any) {
|
||
return {
|
||
...context,
|
||
user: {
|
||
...context.user,
|
||
organizationType: currentUser.organization.type,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
}
|
||
}
|
||
import { ParticipantIsolation } from './security/participant-isolation'
|
||
import { SupplyDataFilter } from './security/supply-data-filter'
|
||
import type { SecurityContext } from './security/types'
|
||
|
||
// Сервисы
|
||
const smsService = new SmsService()
|
||
const dadataService = new DaDataService()
|
||
const marketplaceService = new MarketplaceService()
|
||
|
||
// Функция генерации уникального реферального кода
|
||
const generateReferralCode = async (): Promise<string> => {
|
||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||
let attempts = 0
|
||
const maxAttempts = 10
|
||
|
||
while (attempts < maxAttempts) {
|
||
let code = ''
|
||
for (let i = 0; i < 10; i++) {
|
||
code += chars.charAt(Math.floor(Math.random() * chars.length))
|
||
}
|
||
|
||
// Проверяем уникальность
|
||
const existing = await prisma.organization.findUnique({
|
||
where: { referralCode: code },
|
||
})
|
||
|
||
if (!existing) {
|
||
return code
|
||
}
|
||
|
||
attempts++
|
||
}
|
||
|
||
// Если не удалось сгенерировать уникальный код, используем cuid как fallback
|
||
return `REF${Date.now()}${Math.random().toString(36).substr(2, 5).toUpperCase()}`
|
||
}
|
||
|
||
// Функция для автоматического создания записи склада при новом партнерстве
|
||
const autoCreateWarehouseEntry = async (sellerId: string, fulfillmentId: string) => {
|
||
console.warn(`🏗️ AUTO WAREHOUSE ENTRY: Creating for seller ${sellerId} with fulfillment ${fulfillmentId}`)
|
||
|
||
// Получаем данные селлера
|
||
const sellerOrg = await prisma.organization.findUnique({
|
||
where: { id: sellerId },
|
||
})
|
||
|
||
if (!sellerOrg) {
|
||
throw new Error(`Селлер с ID ${sellerId} не найден`)
|
||
}
|
||
|
||
// Проверяем что не существует уже записи для этого селлера у этого фулфилмента
|
||
// В будущем здесь может быть проверка в отдельной таблице warehouse_entries
|
||
// Пока используем логику проверки через контрагентов
|
||
|
||
// ЛОГИКА ОПРЕДЕЛЕНИЯ НАЗВАНИЯ МАГАЗИНА (консистентно с warehouseData resolver)
|
||
let storeName = sellerOrg.name
|
||
|
||
if (sellerOrg.fullName && sellerOrg.name?.includes('ИП')) {
|
||
// Извлекаем название из скобок, например: "ИП Антипова Д. В. (Renrel)" -> "Renrel"
|
||
const match = sellerOrg.fullName.match(/\(([^)]+)\)/)
|
||
if (match && match[1]) {
|
||
storeName = match[1]
|
||
}
|
||
}
|
||
|
||
// Создаем структуру данных для склада
|
||
const warehouseEntry = {
|
||
id: `warehouse_${sellerId}_${Date.now()}`, // Уникальный ID записи
|
||
storeName: storeName || sellerOrg.fullName || sellerOrg.name,
|
||
storeOwner: sellerOrg.inn || sellerOrg.fullName || sellerOrg.name,
|
||
storeImage: sellerOrg.logoUrl || null,
|
||
storeQuantity: 0, // Пока нет поставок
|
||
partnershipDate: new Date(),
|
||
products: [], // Пустой массив продуктов
|
||
}
|
||
|
||
console.warn('✅ AUTO WAREHOUSE ENTRY CREATED:', {
|
||
sellerId,
|
||
storeName: warehouseEntry.storeName,
|
||
storeOwner: warehouseEntry.storeOwner,
|
||
})
|
||
|
||
// В реальной системе здесь бы была запись в таблицу warehouse_entries
|
||
// Пока возвращаем структуру данных
|
||
return warehouseEntry
|
||
}
|
||
|
||
// Интерфейсы для типизации
|
||
interface Context {
|
||
user?: {
|
||
id: string
|
||
phone: string
|
||
}
|
||
admin?: {
|
||
id: string
|
||
username: string
|
||
}
|
||
}
|
||
|
||
interface CreateEmployeeInput {
|
||
firstName: string
|
||
lastName: string
|
||
middleName?: string
|
||
birthDate?: string
|
||
avatar?: string
|
||
passportPhoto?: string
|
||
passportSeries?: string
|
||
passportNumber?: string
|
||
passportIssued?: string
|
||
passportDate?: string
|
||
address?: string
|
||
position: string
|
||
department?: string
|
||
hireDate: string
|
||
salary?: number
|
||
phone: string
|
||
email?: string
|
||
telegram?: string
|
||
whatsapp?: string
|
||
emergencyContact?: string
|
||
emergencyPhone?: string
|
||
}
|
||
|
||
interface UpdateEmployeeInput {
|
||
firstName?: string
|
||
lastName?: string
|
||
middleName?: string
|
||
birthDate?: string
|
||
avatar?: string
|
||
passportPhoto?: string
|
||
passportSeries?: string
|
||
passportNumber?: string
|
||
passportIssued?: string
|
||
passportDate?: string
|
||
address?: string
|
||
position?: string
|
||
department?: string
|
||
hireDate?: string
|
||
salary?: number
|
||
status?: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED'
|
||
phone?: string
|
||
email?: string
|
||
telegram?: string
|
||
whatsapp?: string
|
||
emergencyContact?: string
|
||
emergencyPhone?: string
|
||
}
|
||
|
||
interface UpdateScheduleInput {
|
||
employeeId: string
|
||
date: string
|
||
status: 'WORK' | 'WEEKEND' | 'VACATION' | 'SICK' | 'ABSENT'
|
||
hoursWorked?: number
|
||
overtimeHours?: number
|
||
notes?: string
|
||
}
|
||
|
||
interface AuthTokenPayload {
|
||
userId: string
|
||
phone: string
|
||
}
|
||
|
||
// JWT утилиты
|
||
const generateToken = (payload: AuthTokenPayload): string => {
|
||
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '30d' })
|
||
}
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
const verifyToken = (token: string): AuthTokenPayload => {
|
||
try {
|
||
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
} catch (error) {
|
||
throw new GraphQLError('Недействительный токен', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
}
|
||
|
||
// Скалярный тип для JSON
|
||
const JSONScalar = new GraphQLScalarType({
|
||
name: 'JSON',
|
||
description: 'JSON custom scalar type',
|
||
serialize(value: unknown) {
|
||
return value // значение отправляется клиенту
|
||
},
|
||
parseValue(value: unknown) {
|
||
return value // значение получено от клиента
|
||
},
|
||
parseLiteral(ast) {
|
||
switch (ast.kind) {
|
||
case Kind.STRING:
|
||
case Kind.BOOLEAN:
|
||
return ast.value
|
||
case Kind.INT:
|
||
case Kind.FLOAT:
|
||
return parseFloat(ast.value)
|
||
case Kind.OBJECT: {
|
||
const value = Object.create(null)
|
||
ast.fields.forEach((field) => {
|
||
value[field.name.value] = parseLiteral(field.value)
|
||
})
|
||
return value
|
||
}
|
||
case Kind.LIST:
|
||
return ast.values.map(parseLiteral)
|
||
default:
|
||
return null
|
||
}
|
||
},
|
||
})
|
||
|
||
// Скалярный тип для DateTime
|
||
const DateTimeScalar = new GraphQLScalarType({
|
||
name: 'DateTime',
|
||
description: 'DateTime custom scalar type',
|
||
serialize(value: unknown) {
|
||
if (value instanceof Date) {
|
||
return value.toISOString() // значение отправляется клиенту как ISO строка
|
||
}
|
||
return value
|
||
},
|
||
parseValue(value: unknown) {
|
||
if (typeof value === 'string') {
|
||
return new Date(value) // значение получено от клиента, парсим как дату
|
||
}
|
||
return value
|
||
},
|
||
parseLiteral(ast) {
|
||
if (ast.kind === Kind.STRING) {
|
||
return new Date(ast.value) // AST значение как дата
|
||
}
|
||
return null
|
||
},
|
||
})
|
||
|
||
function parseLiteral(ast: unknown): unknown {
|
||
const astNode = ast as {
|
||
kind: string
|
||
value?: unknown
|
||
fields?: unknown[]
|
||
values?: unknown[]
|
||
}
|
||
|
||
switch (astNode.kind) {
|
||
case Kind.STRING:
|
||
case Kind.BOOLEAN:
|
||
return astNode.value
|
||
case Kind.INT:
|
||
case Kind.FLOAT:
|
||
return parseFloat(astNode.value as string)
|
||
case Kind.OBJECT: {
|
||
const value = Object.create(null)
|
||
if (astNode.fields) {
|
||
astNode.fields.forEach((field: unknown) => {
|
||
const fieldNode = field as {
|
||
name: { value: string }
|
||
value: unknown
|
||
}
|
||
value[fieldNode.name.value] = parseLiteral(fieldNode.value)
|
||
})
|
||
}
|
||
return value
|
||
}
|
||
case Kind.LIST:
|
||
return (ast as { values: unknown[] }).values.map(parseLiteral)
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
export const resolvers = {
|
||
JSON: JSONScalar,
|
||
DateTime: DateTimeScalar,
|
||
|
||
Query: {
|
||
me: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
return await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
},
|
||
|
||
organization: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const organization = await prisma.organization.findUnique({
|
||
where: { id: args.id },
|
||
include: {
|
||
apiKeys: true,
|
||
users: true,
|
||
},
|
||
})
|
||
|
||
if (!organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Проверяем, что пользователь имеет доступ к этой организации
|
||
const hasAccess = organization.users.some((user) => user.id === context.user!.id)
|
||
if (!hasAccess) {
|
||
throw new GraphQLError('Нет доступа к этой организации', {
|
||
extensions: { code: 'FORBIDDEN' },
|
||
})
|
||
}
|
||
|
||
return organization
|
||
},
|
||
|
||
// Поиск организаций по типу для добавления в контрагенты
|
||
searchOrganizations: async (_: unknown, args: { type?: string; search?: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
// Получаем текущую организацию пользователя
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Получаем уже существующих контрагентов для добавления флага
|
||
const existingCounterparties = await prisma.counterparty.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
select: { counterpartyId: true },
|
||
})
|
||
|
||
const existingCounterpartyIds = existingCounterparties.map((c) => c.counterpartyId)
|
||
|
||
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
|
||
const outgoingRequests = await prisma.counterpartyRequest.findMany({
|
||
where: {
|
||
senderId: currentUser.organization.id,
|
||
status: 'PENDING',
|
||
},
|
||
select: { receiverId: true },
|
||
})
|
||
|
||
const outgoingRequestIds = outgoingRequests.map((r) => r.receiverId)
|
||
|
||
// Получаем входящие заявки для добавления флага hasIncomingRequest
|
||
const incomingRequests = await prisma.counterpartyRequest.findMany({
|
||
where: {
|
||
receiverId: currentUser.organization.id,
|
||
status: 'PENDING',
|
||
},
|
||
select: { senderId: true },
|
||
})
|
||
|
||
const incomingRequestIds = incomingRequests.map((r) => r.senderId)
|
||
|
||
const where: Record<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('У пользователя нет организации')
|
||
}
|
||
|
||
console.warn('🔍 SUPPLY ORDERS RESOLVER:', {
|
||
userId: context.user.id,
|
||
organizationType: currentUser.organization.type,
|
||
organizationId: currentUser.organization.id,
|
||
organizationName: currentUser.organization.name,
|
||
})
|
||
|
||
try {
|
||
// Возвращаем заказы где текущая организация является заказчиком, поставщиком, получателем или логистическим партнером
|
||
const orders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
OR: [
|
||
{ organizationId: currentUser.organization.id }, // Заказы созданные организацией
|
||
{ partnerId: currentUser.organization.id }, // Заказы где организация - поставщик
|
||
{ fulfillmentCenterId: currentUser.organization.id }, // Заказы где организация - получатель (фулфилмент)
|
||
{ logisticsPartnerId: currentUser.organization.id }, // Заказы где организация - логистический партнер
|
||
],
|
||
},
|
||
include: {
|
||
partner: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
fulfillmentCenter: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
console.warn('📦 SUPPLY ORDERS FOUND:', {
|
||
totalOrders: orders.length,
|
||
ordersByRole: {
|
||
asCreator: orders.filter((o) => o.organizationId === currentUser.organization.id).length,
|
||
asPartner: orders.filter((o) => o.partnerId === currentUser.organization.id).length,
|
||
asFulfillment: orders.filter((o) => o.fulfillmentCenterId === currentUser.organization.id).length,
|
||
asLogistics: orders.filter((o) => o.logisticsPartnerId === currentUser.organization.id).length,
|
||
},
|
||
orderStatuses: orders.reduce((acc: any, order) => {
|
||
acc[order.status] = (acc[order.status] || 0) + 1
|
||
return acc
|
||
}, {}),
|
||
orderIds: orders.map((o) => o.id),
|
||
})
|
||
|
||
return orders
|
||
} catch (error) {
|
||
console.error('❌ ERROR IN SUPPLY ORDERS RESOLVER:', error)
|
||
throw new GraphQLError(`Ошибка получения заказов поставок: ${error}`)
|
||
}
|
||
},
|
||
|
||
// Счетчик поставок, требующих одобрения
|
||
pendingSuppliesCount: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Считаем заказы поставок, требующие действий
|
||
|
||
// Расходники фулфилмента (созданные нами для себя) - требуют действий по статусам
|
||
const ourSupplyOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
organizationId: currentUser.organization.id, // Создали мы
|
||
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
|
||
status: { in: ['CONFIRMED', 'IN_TRANSIT'] }, // Подтверждено или в пути
|
||
},
|
||
})
|
||
|
||
// Расходники селлеров (созданные другими для нас) - требуют действий фулфилмента
|
||
const sellerSupplyOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
|
||
organizationId: { not: currentUser.organization.id }, // Создали НЕ мы
|
||
status: {
|
||
in: [
|
||
'SUPPLIER_APPROVED', // Поставщик подтвердил - нужно назначить логистику
|
||
'IN_TRANSIT', // В пути - нужно подтвердить получение
|
||
],
|
||
},
|
||
},
|
||
})
|
||
|
||
// 🔔 ВХОДЯЩИЕ ЗАКАЗЫ ДЛЯ ПОСТАВЩИКОВ (WHOLESALE) - требуют подтверждения
|
||
const incomingSupplierOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
partnerId: currentUser.organization.id, // Мы - поставщик
|
||
status: 'PENDING', // Ожидает подтверждения от поставщика
|
||
},
|
||
})
|
||
|
||
// 🚚 ЛОГИСТИЧЕСКИЕ ЗАЯВКИ ДЛЯ ЛОГИСТИКИ (LOGIST) - требуют действий логистики
|
||
const logisticsOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
logisticsPartnerId: currentUser.organization.id, // Мы - назначенная логистика
|
||
status: {
|
||
in: [
|
||
'CONFIRMED', // Подтверждено фулфилментом - нужно подтвердить логистикой
|
||
'LOGISTICS_CONFIRMED', // Подтверждено логистикой - нужно забрать товар у поставщика
|
||
],
|
||
},
|
||
},
|
||
})
|
||
|
||
// Общий счетчик поставок в зависимости от типа организации
|
||
let pendingSupplyOrders = 0
|
||
if (currentUser.organization.type === 'FULFILLMENT') {
|
||
pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders
|
||
} else if (currentUser.organization.type === 'WHOLESALE') {
|
||
pendingSupplyOrders = incomingSupplierOrders
|
||
} else if (currentUser.organization.type === 'LOGIST') {
|
||
pendingSupplyOrders = logisticsOrders
|
||
} else if (currentUser.organization.type === 'SELLER') {
|
||
pendingSupplyOrders = 0 // Селлеры не подтверждают поставки, только отслеживают
|
||
}
|
||
|
||
// Считаем входящие заявки на партнерство со статусом PENDING
|
||
const pendingIncomingRequests = await prisma.counterpartyRequest.count({
|
||
where: {
|
||
receiverId: currentUser.organization.id,
|
||
status: 'PENDING',
|
||
},
|
||
})
|
||
|
||
return {
|
||
supplyOrders: pendingSupplyOrders,
|
||
ourSupplyOrders: ourSupplyOrders, // Расходники фулфилмента
|
||
sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров
|
||
incomingSupplierOrders: incomingSupplierOrders, // 🔔 Входящие заказы для поставщиков
|
||
logisticsOrders: logisticsOrders, // 🚚 Логистические заявки для логистики
|
||
incomingRequests: pendingIncomingRequests,
|
||
total: pendingSupplyOrders + pendingIncomingRequests,
|
||
}
|
||
},
|
||
|
||
// Статистика склада фулфилмента с изменениями за сутки
|
||
fulfillmentWarehouseStats: async (_: unknown, __: unknown, context: Context) => {
|
||
console.warn('🔥 FULFILLMENT WAREHOUSE STATS RESOLVER CALLED')
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступ разрешен только для фулфилмент-центров')
|
||
}
|
||
|
||
const organizationId = currentUser.organization.id
|
||
|
||
// Получаем дату начала суток (24 часа назад)
|
||
const oneDayAgo = new Date()
|
||
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
|
||
|
||
console.warn(`🏢 Organization ID: ${organizationId}, Date 24h ago: ${oneDayAgo.toISOString()}`)
|
||
|
||
// Сначала проверим ВСЕ заказы поставок
|
||
const allSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: { status: 'DELIVERED' },
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
organization: { select: { id: true, name: true, type: true } },
|
||
},
|
||
})
|
||
console.warn(`📦 ALL DELIVERED ORDERS: ${allSupplyOrders.length}`)
|
||
allSupplyOrders.forEach((order) => {
|
||
console.warn(
|
||
` Order ${order.id}: org=${order.organizationId} (${order.organization?.name}), fulfillment=${order.fulfillmentCenterId}, items=${order.items.length}`,
|
||
)
|
||
})
|
||
|
||
// Продукты (товары от селлеров) - заказы К нам, но исключаем расходники фулфилмента
|
||
const sellerDeliveredOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
fulfillmentCenterId: organizationId, // Доставлено к нам (фулфилменту)
|
||
organizationId: { not: organizationId }, // ИСПРАВЛЕНО: исключаем заказы самого фулфилмента
|
||
status: 'DELIVERED',
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
console.warn(`🛒 SELLER ORDERS TO FULFILLMENT: ${sellerDeliveredOrders.length}`)
|
||
|
||
const productsCount = sellerDeliveredOrders.reduce(
|
||
(sum, order) =>
|
||
sum +
|
||
order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'PRODUCT' ? item.quantity : 0), 0),
|
||
0,
|
||
)
|
||
// Изменения товаров за сутки (от селлеров)
|
||
const recentSellerDeliveredOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
fulfillmentCenterId: organizationId, // К нам
|
||
organizationId: { not: organizationId }, // От селлеров
|
||
status: 'DELIVERED',
|
||
updatedAt: { gte: oneDayAgo },
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
const productsChangeToday = recentSellerDeliveredOrders.reduce(
|
||
(sum, order) =>
|
||
sum +
|
||
order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'PRODUCT' ? item.quantity : 0), 0),
|
||
0,
|
||
)
|
||
|
||
// Товары (готовые товары = все продукты, не расходники)
|
||
const goodsCount = productsCount // Готовые товары = все продукты
|
||
const goodsChangeToday = productsChangeToday // Изменения товаров = изменения продуктов
|
||
|
||
// Брак
|
||
const defectsCount = 0 // TODO: реальные данные о браке
|
||
const defectsChangeToday = 0
|
||
|
||
// Возвраты с ПВЗ
|
||
const pvzReturnsCount = 0 // TODO: реальные данные о возвратах
|
||
const pvzReturnsChangeToday = 0
|
||
|
||
// Расходники фулфилмента - заказы ОТ фулфилмента К поставщикам, НО доставленные на склад фулфилмента
|
||
// Согласно правилам: фулфилмент заказывает расходники у поставщиков для своих операционных нужд
|
||
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Заказчик = фулфилмент
|
||
fulfillmentCenterId: organizationId, // ИСПРАВЛЕНО: доставлено НА склад фулфилмента
|
||
status: 'DELIVERED',
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
console.warn(`🏭 FULFILLMENT SUPPLY ORDERS: ${fulfillmentSupplyOrders.length}`)
|
||
|
||
// Подсчитываем количество из таблицы Supply (актуальные остатки на складе фулфилмента)
|
||
// ИСПРАВЛЕНО: считаем только расходники фулфилмента, исключаем расходники селлеров
|
||
const fulfillmentSuppliesFromWarehouse = await prisma.supply.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Склад фулфилмента
|
||
type: 'FULFILLMENT_CONSUMABLES', // ТОЛЬКО расходники фулфилмента
|
||
},
|
||
})
|
||
|
||
const fulfillmentSuppliesCount = fulfillmentSuppliesFromWarehouse.reduce(
|
||
(sum, supply) => sum + (supply.currentStock || 0),
|
||
0,
|
||
)
|
||
|
||
console.warn(
|
||
`🔥 FULFILLMENT SUPPLIES DEBUG: organizationId=${organizationId}, ordersCount=${fulfillmentSupplyOrders.length}, warehouseCount=${fulfillmentSuppliesFromWarehouse.length}, totalStock=${fulfillmentSuppliesCount}`,
|
||
)
|
||
console.warn(
|
||
'📦 FULFILLMENT SUPPLIES BREAKDOWN:',
|
||
fulfillmentSuppliesFromWarehouse.map((supply) => ({
|
||
name: supply.name,
|
||
currentStock: supply.currentStock,
|
||
supplier: supply.supplier,
|
||
})),
|
||
)
|
||
|
||
// Изменения расходников фулфилмента за сутки (ПРИБЫЛО)
|
||
// Ищем заказы фулфилмента, доставленные на его склад за последние сутки
|
||
const fulfillmentSuppliesReceivedToday = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Заказчик = фулфилмент
|
||
fulfillmentCenterId: organizationId, // ИСПРАВЛЕНО: доставлено НА склад фулфилмента
|
||
status: 'DELIVERED',
|
||
updatedAt: { gte: oneDayAgo },
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
const fulfillmentSuppliesChangeToday = fulfillmentSuppliesReceivedToday.reduce(
|
||
(sum, order) =>
|
||
sum +
|
||
order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'CONSUMABLE' ? item.quantity : 0), 0),
|
||
0,
|
||
)
|
||
|
||
console.warn(
|
||
`📊 FULFILLMENT SUPPLIES RECEIVED TODAY (ПРИБЫЛО): ${fulfillmentSuppliesReceivedToday.length} orders, ${fulfillmentSuppliesChangeToday} items`,
|
||
)
|
||
|
||
// Расходники селлеров - получаем из таблицы Supply (актуальные остатки на складе фулфилмента)
|
||
// ИСПРАВЛЕНО: считаем из Supply с типом SELLER_CONSUMABLES
|
||
const sellerSuppliesFromWarehouse = await prisma.supply.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Склад фулфилмента
|
||
type: 'SELLER_CONSUMABLES', // ТОЛЬКО расходники селлеров
|
||
},
|
||
})
|
||
|
||
const sellerSuppliesCount = sellerSuppliesFromWarehouse.reduce(
|
||
(sum, supply) => sum + (supply.currentStock || 0),
|
||
0,
|
||
)
|
||
|
||
console.warn(`💼 SELLER SUPPLIES DEBUG: totalCount=${sellerSuppliesCount} (from Supply warehouse)`)
|
||
|
||
// Изменения расходников селлеров за сутки - считаем из Supply записей, созданных за сутки
|
||
const sellerSuppliesReceivedToday = await prisma.supply.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Склад фулфилмента
|
||
type: 'SELLER_CONSUMABLES', // ТОЛЬКО расходники селлеров
|
||
createdAt: { gte: oneDayAgo }, // Созданы за последние сутки
|
||
},
|
||
})
|
||
|
||
const sellerSuppliesChangeToday = sellerSuppliesReceivedToday.reduce(
|
||
(sum, supply) => sum + (supply.currentStock || 0),
|
||
0,
|
||
)
|
||
|
||
console.warn(
|
||
`📊 SELLER SUPPLIES RECEIVED TODAY: ${sellerSuppliesReceivedToday.length} supplies, ${sellerSuppliesChangeToday} items`,
|
||
)
|
||
|
||
// Вычисляем процентные изменения
|
||
const calculatePercentChange = (current: number, change: number): number => {
|
||
if (current === 0) return change > 0 ? 100 : 0
|
||
return (change / current) * 100
|
||
}
|
||
|
||
const result = {
|
||
products: {
|
||
current: productsCount,
|
||
change: productsChangeToday,
|
||
percentChange: calculatePercentChange(productsCount, productsChangeToday),
|
||
},
|
||
goods: {
|
||
current: goodsCount,
|
||
change: goodsChangeToday,
|
||
percentChange: calculatePercentChange(goodsCount, goodsChangeToday),
|
||
},
|
||
defects: {
|
||
current: defectsCount,
|
||
change: defectsChangeToday,
|
||
percentChange: calculatePercentChange(defectsCount, defectsChangeToday),
|
||
},
|
||
pvzReturns: {
|
||
current: pvzReturnsCount,
|
||
change: pvzReturnsChangeToday,
|
||
percentChange: calculatePercentChange(pvzReturnsCount, pvzReturnsChangeToday),
|
||
},
|
||
fulfillmentSupplies: {
|
||
current: fulfillmentSuppliesCount,
|
||
change: fulfillmentSuppliesChangeToday,
|
||
percentChange: calculatePercentChange(fulfillmentSuppliesCount, fulfillmentSuppliesChangeToday),
|
||
},
|
||
sellerSupplies: {
|
||
current: sellerSuppliesCount,
|
||
change: sellerSuppliesChangeToday,
|
||
percentChange: calculatePercentChange(sellerSuppliesCount, sellerSuppliesChangeToday),
|
||
},
|
||
}
|
||
|
||
console.warn('🏁 FINAL WAREHOUSE STATS RESULT:', JSON.stringify(result, null, 2))
|
||
|
||
return result
|
||
},
|
||
|
||
// Движения товаров (прибыло/убыло) за период
|
||
supplyMovements: async (_: unknown, args: { period?: string }, context: Context) => {
|
||
console.warn('🔄 SUPPLY MOVEMENTS RESOLVER CALLED with period:', args.period)
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступ разрешен только фулфилмент центрам')
|
||
}
|
||
|
||
const organizationId = currentUser.organization.id
|
||
console.warn(`🏢 SUPPLY MOVEMENTS for organization: ${organizationId}`)
|
||
|
||
// Определяем период (по умолчанию 24 часа)
|
||
const periodHours = args.period === '7d' ? 168 : args.period === '30d' ? 720 : 24
|
||
const periodAgo = new Date(Date.now() - periodHours * 60 * 60 * 1000)
|
||
|
||
// ПРИБЫЛО: Поставки НА фулфилмент (статус DELIVERED за период)
|
||
const arrivedOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
fulfillmentCenterId: organizationId,
|
||
status: 'DELIVERED',
|
||
updatedAt: { gte: periodAgo },
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
console.warn(`📦 ARRIVED ORDERS: ${arrivedOrders.length}`)
|
||
|
||
// Подсчитываем прибыло по типам
|
||
const arrived = {
|
||
products: 0,
|
||
goods: 0,
|
||
defects: 0,
|
||
pvzReturns: 0,
|
||
fulfillmentSupplies: 0,
|
||
sellerSupplies: 0,
|
||
}
|
||
|
||
arrivedOrders.forEach((order) => {
|
||
order.items.forEach((item) => {
|
||
const quantity = item.quantity
|
||
const productType = item.product?.type
|
||
|
||
if (productType === 'PRODUCT') arrived.products += quantity
|
||
else if (productType === 'GOODS') arrived.goods += quantity
|
||
else if (productType === 'DEFECT') arrived.defects += quantity
|
||
else if (productType === 'PVZ_RETURN') arrived.pvzReturns += quantity
|
||
else if (productType === 'CONSUMABLE') {
|
||
// Определяем тип расходника по заказчику
|
||
if (order.organizationId === organizationId) {
|
||
arrived.fulfillmentSupplies += quantity
|
||
} else {
|
||
arrived.sellerSupplies += quantity
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
// УБЫЛО: Поставки НА маркетплейсы (по статусам отгрузки)
|
||
// TODO: Пока возвращаем заглушки, нужно реализовать логику отгрузок
|
||
const departed = {
|
||
products: 0, // TODO: считать из отгрузок на WB/Ozon
|
||
goods: 0,
|
||
defects: 0,
|
||
pvzReturns: 0,
|
||
fulfillmentSupplies: 0,
|
||
sellerSupplies: 0,
|
||
}
|
||
|
||
console.warn('📊 SUPPLY MOVEMENTS RESULT:', { arrived, departed })
|
||
|
||
return {
|
||
arrived,
|
||
departed,
|
||
}
|
||
},
|
||
|
||
// Логистика организации
|
||
myLogistics: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
return await prisma.logistics.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
|
||
// Логистические партнеры (организации-логисты)
|
||
logisticsPartners: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
// Получаем все организации типа LOGIST
|
||
return await prisma.organization.findMany({
|
||
where: {
|
||
type: 'LOGIST',
|
||
// Убираем фильтр по статусу пока не определим правильные значения
|
||
},
|
||
orderBy: { createdAt: 'desc' }, // Сортируем по дате создания вместо name
|
||
})
|
||
},
|
||
|
||
// Мои поставки Wildberries
|
||
myWildberriesSupplies: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
return await prisma.wildberriesSupply.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
organization: true,
|
||
cards: true,
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
|
||
// Расходники селлеров на складе фулфилмента (новый resolver)
|
||
sellerSuppliesOnWarehouse: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Только фулфилмент может получать расходники селлеров на своем складе
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступ разрешен только для фулфилмент-центров')
|
||
}
|
||
|
||
// ИСПРАВЛЕНО: Усиленная фильтрация расходников селлеров
|
||
const sellerSupplies = await prisma.supply.findMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id, // На складе этого фулфилмента
|
||
type: 'SELLER_CONSUMABLES' as const, // Только расходники селлеров
|
||
sellerOwnerId: { not: null }, // ОБЯЗАТЕЛЬНО должен быть владелец-селлер
|
||
},
|
||
include: {
|
||
organization: true, // Фулфилмент-центр (хранитель)
|
||
sellerOwner: true, // Селлер-владелец расходников
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
// Логирование для отладки
|
||
console.warn('🔍 ИСПРАВЛЕНО: Запрос расходников селлеров на складе фулфилмента:', {
|
||
fulfillmentId: currentUser.organization.id,
|
||
fulfillmentName: currentUser.organization.name,
|
||
totalSupplies: sellerSupplies.length,
|
||
sellerSupplies: sellerSupplies.map((supply) => ({
|
||
id: supply.id,
|
||
name: supply.name,
|
||
type: supply.type,
|
||
sellerOwnerId: supply.sellerOwnerId,
|
||
sellerOwnerName: supply.sellerOwner?.name || supply.sellerOwner?.fullName,
|
||
currentStock: supply.currentStock,
|
||
})),
|
||
})
|
||
|
||
// ДВОЙНАЯ ПРОВЕРКА: Фильтруем на уровне кода для гарантии
|
||
const filteredSupplies = sellerSupplies.filter((supply) => {
|
||
const isValid =
|
||
supply.type === 'SELLER_CONSUMABLES' && supply.sellerOwnerId != null && supply.sellerOwner != null
|
||
|
||
if (!isValid) {
|
||
console.warn('⚠️ ОТФИЛЬТРОВАН некорректный расходник:', {
|
||
id: supply.id,
|
||
name: supply.name,
|
||
type: supply.type,
|
||
sellerOwnerId: supply.sellerOwnerId,
|
||
hasSellerOwner: !!supply.sellerOwner,
|
||
})
|
||
}
|
||
|
||
return isValid
|
||
})
|
||
|
||
console.warn('✅ ФИНАЛЬНЫЙ РЕЗУЛЬТАТ после фильтрации:', {
|
||
originalCount: sellerSupplies.length,
|
||
filteredCount: filteredSupplies.length,
|
||
removedCount: sellerSupplies.length - filteredSupplies.length,
|
||
})
|
||
|
||
return filteredSupplies
|
||
},
|
||
|
||
// Мои товары и расходники (для поставщиков)
|
||
myProducts: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это поставщик
|
||
if (currentUser.organization.type !== 'WHOLESALE') {
|
||
throw new GraphQLError('Товары доступны только для поставщиков')
|
||
}
|
||
|
||
const products = await prisma.product.findMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
// Показываем и товары, и расходники поставщика
|
||
},
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
console.warn('🔥 MY_PRODUCTS RESOLVER DEBUG:', {
|
||
userId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
organizationName: currentUser.organization.name,
|
||
totalProducts: products.length,
|
||
productTypes: products.map((p) => ({
|
||
id: p.id,
|
||
name: p.name,
|
||
article: p.article,
|
||
type: p.type,
|
||
isActive: p.isActive,
|
||
createdAt: p.createdAt,
|
||
})),
|
||
})
|
||
|
||
return products
|
||
},
|
||
|
||
// Товары на складе фулфилмента (из доставленных заказов поставок)
|
||
warehouseProducts: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Товары склада доступны только для фулфилмент центров')
|
||
}
|
||
|
||
// Получаем все доставленные заказы поставок, где этот фулфилмент центр является получателем
|
||
const deliveredSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
fulfillmentCenterId: currentUser.organization.id,
|
||
status: 'DELIVERED', // Только доставленные заказы
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true, // Включаем информацию о поставщике
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true, // Селлер, который сделал заказ
|
||
partner: true, // Поставщик товаров
|
||
},
|
||
})
|
||
|
||
// Собираем все товары из доставленных заказов
|
||
const allProducts: unknown[] = []
|
||
|
||
console.warn('🔍 Резолвер warehouseProducts (доставленные заказы):', {
|
||
currentUserId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
deliveredOrdersCount: deliveredSupplyOrders.length,
|
||
orders: deliveredSupplyOrders.map((order) => ({
|
||
id: order.id,
|
||
sellerName: order.organization.name || order.organization.fullName,
|
||
supplierName: order.partner.name || order.partner.fullName,
|
||
status: order.status,
|
||
itemsCount: order.items.length,
|
||
deliveryDate: order.deliveryDate,
|
||
})),
|
||
})
|
||
|
||
for (const order of deliveredSupplyOrders) {
|
||
console.warn(
|
||
`📦 Заказ от селлера ${order.organization.name} у поставщика ${order.partner.name}:`,
|
||
order.items.map((item) => ({
|
||
productId: item.product.id,
|
||
productName: item.product.name,
|
||
article: item.product.article,
|
||
orderedQuantity: item.quantity,
|
||
price: item.price,
|
||
})),
|
||
)
|
||
|
||
for (const item of order.items) {
|
||
// Добавляем только товары типа PRODUCT, исключаем расходники
|
||
if (item.product.type === 'PRODUCT') {
|
||
allProducts.push({
|
||
...item.product,
|
||
// Дополнительная информация о заказе
|
||
orderedQuantity: item.quantity,
|
||
orderedPrice: item.price,
|
||
orderId: order.id,
|
||
orderDate: order.deliveryDate,
|
||
seller: order.organization, // Селлер, который заказал
|
||
supplier: order.partner, // Поставщик товара
|
||
// Для совместимости с существующим интерфейсом
|
||
organization: order.organization, // Указываем селлера как владельца
|
||
})
|
||
} else {
|
||
console.warn('🚫 Исключен расходник из основного склада фулфилмента:', {
|
||
name: item.product.name,
|
||
type: item.product.type,
|
||
orderId: order.id,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
console.warn('✅ Итого товаров на складе фулфилмента (из доставленных заказов):', allProducts.length)
|
||
return allProducts
|
||
},
|
||
|
||
// Данные склада с партнерами (3-уровневая иерархия)
|
||
warehouseData: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Данные склада доступны только для фулфилмент центров')
|
||
}
|
||
|
||
console.warn('🏪 WAREHOUSE DATA: Получение данных склада для фулфилмента', currentUser.organization.id)
|
||
|
||
// Получаем всех партнеров-селлеров
|
||
const counterparties = await prisma.counterparty.findMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: {
|
||
counterparty: true,
|
||
},
|
||
})
|
||
|
||
const sellerPartners = counterparties.filter((c) => c.counterparty.type === 'SELLER')
|
||
|
||
console.warn('🤝 PARTNERS FOUND:', {
|
||
totalCounterparties: counterparties.length,
|
||
sellerPartners: sellerPartners.length,
|
||
sellers: sellerPartners.map((p) => ({
|
||
id: p.counterparty.id,
|
||
name: p.counterparty.name,
|
||
fullName: p.counterparty.fullName,
|
||
inn: p.counterparty.inn,
|
||
})),
|
||
})
|
||
|
||
// Создаем данные склада для каждого партнера-селлера
|
||
const stores = sellerPartners.map((partner) => {
|
||
const org = partner.counterparty
|
||
|
||
// ЛОГИКА ОПРЕДЕЛЕНИЯ НАЗВАНИЯ МАГАЗИНА:
|
||
// 1. Если есть name и оно не содержит "ИП" - используем name
|
||
// 2. Если есть fullName и name содержит "ИП" - извлекаем из fullName название в скобках
|
||
// 3. Fallback к name или fullName
|
||
let storeName = org.name
|
||
|
||
if (org.fullName && org.name?.includes('ИП')) {
|
||
// Извлекаем название из скобок, например: "ИП Антипова Д. В. (Renrel)" -> "Renrel"
|
||
const match = org.fullName.match(/\(([^)]+)\)/)
|
||
if (match && match[1]) {
|
||
storeName = match[1]
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: `store_${org.id}`,
|
||
storeName: storeName || org.fullName || org.name,
|
||
storeOwner: org.inn || org.fullName || org.name,
|
||
storeImage: org.logoUrl || null,
|
||
storeQuantity: 0, // Пока без поставок
|
||
partnershipDate: partner.createdAt || new Date(),
|
||
products: [], // Пустой массив продуктов
|
||
}
|
||
})
|
||
|
||
// Сортировка: новые партнеры (quantity = 0) в самом верху
|
||
stores.sort((a, b) => {
|
||
if (a.storeQuantity === 0 && b.storeQuantity > 0) return -1
|
||
if (a.storeQuantity > 0 && b.storeQuantity === 0) return 1
|
||
return b.storeQuantity - a.storeQuantity
|
||
})
|
||
|
||
console.warn('📦 WAREHOUSE STORES CREATED:', {
|
||
storesCount: stores.length,
|
||
storesPreview: stores.slice(0, 3).map((s) => ({
|
||
storeName: s.storeName,
|
||
storeOwner: s.storeOwner,
|
||
storeQuantity: s.storeQuantity,
|
||
})),
|
||
})
|
||
|
||
return {
|
||
stores,
|
||
}
|
||
},
|
||
|
||
// Все товары и расходники поставщиков для маркета
|
||
allProducts: async (_: unknown, args: { search?: string; category?: string }, context: Context) => {
|
||
console.warn('🛍️ ALL_PRODUCTS RESOLVER - ВЫЗВАН:', {
|
||
userId: context.user?.id,
|
||
search: args.search,
|
||
category: args.category,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const where: Record<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
|
||
},
|
||
|
||
// Получить партнерскую ссылку текущего пользователя
|
||
myPartnerLink: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user?.organizationId) {
|
||
throw new GraphQLError('Требуется авторизация и организация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const organization = await prisma.organization.findUnique({
|
||
where: { id: context.user.organizationId },
|
||
select: { referralCode: true },
|
||
})
|
||
|
||
if (!organization?.referralCode) {
|
||
throw new GraphQLError('Реферальный код не найден')
|
||
}
|
||
|
||
return `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/register?partner=${organization.referralCode}`
|
||
},
|
||
|
||
// Получить реферальную ссылку
|
||
myReferralLink: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user?.organizationId) {
|
||
return 'http://localhost:3000/register?ref=PLEASE_LOGIN'
|
||
}
|
||
|
||
const organization = await prisma.organization.findUnique({
|
||
where: { id: context.user.organizationId },
|
||
select: { referralCode: true },
|
||
})
|
||
|
||
if (!organization?.referralCode) {
|
||
throw new GraphQLError('Реферальный код не найден')
|
||
}
|
||
|
||
return `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/register?ref=${organization.referralCode}`
|
||
},
|
||
|
||
// Статистика по рефералам
|
||
myReferralStats: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user?.organizationId) {
|
||
throw new GraphQLError('Требуется авторизация и организация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Получаем текущие реферальные очки организации
|
||
const organization = await prisma.organization.findUnique({
|
||
where: { id: context.user.organizationId },
|
||
select: { referralPoints: true },
|
||
})
|
||
|
||
// Получаем все транзакции где эта организация - реферер
|
||
const transactions = await prisma.referralTransaction.findMany({
|
||
where: { referrerId: context.user.organizationId },
|
||
include: {
|
||
referral: {
|
||
select: {
|
||
type: true,
|
||
createdAt: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// Подсчитываем статистику
|
||
const totalSpheres = organization?.referralPoints || 0
|
||
const totalPartners = transactions.length
|
||
|
||
// Партнеры за последний месяц
|
||
const lastMonth = new Date()
|
||
lastMonth.setMonth(lastMonth.getMonth() - 1)
|
||
const monthlyPartners = transactions.filter((tx) => tx.createdAt > lastMonth).length
|
||
const monthlySpheres = transactions
|
||
.filter((tx) => tx.createdAt > lastMonth)
|
||
.reduce((sum, tx) => sum + tx.points, 0)
|
||
|
||
// Группировка по типам организаций
|
||
const typeStats: Record<string, { count: number; spheres: number }> = {}
|
||
transactions.forEach((tx) => {
|
||
const type = tx.referral.type
|
||
if (!typeStats[type]) {
|
||
typeStats[type] = { count: 0, spheres: 0 }
|
||
}
|
||
typeStats[type].count++
|
||
typeStats[type].spheres += tx.points
|
||
})
|
||
|
||
// Группировка по источникам
|
||
const sourceStats: Record<string, { count: number; spheres: number }> = {}
|
||
transactions.forEach((tx) => {
|
||
const source = tx.type === 'REGISTRATION' ? 'REFERRAL_LINK' : 'AUTO_BUSINESS'
|
||
if (!sourceStats[source]) {
|
||
sourceStats[source] = { count: 0, spheres: 0 }
|
||
}
|
||
sourceStats[source].count++
|
||
sourceStats[source].spheres += tx.points
|
||
})
|
||
|
||
return {
|
||
totalPartners,
|
||
totalSpheres,
|
||
monthlyPartners,
|
||
monthlySpheres,
|
||
referralsByType: [
|
||
{ type: 'SELLER', count: typeStats['SELLER']?.count || 0, spheres: typeStats['SELLER']?.spheres || 0 },
|
||
{
|
||
type: 'WHOLESALE',
|
||
count: typeStats['WHOLESALE']?.count || 0,
|
||
spheres: typeStats['WHOLESALE']?.spheres || 0,
|
||
},
|
||
{
|
||
type: 'FULFILLMENT',
|
||
count: typeStats['FULFILLMENT']?.count || 0,
|
||
spheres: typeStats['FULFILLMENT']?.spheres || 0,
|
||
},
|
||
{ type: 'LOGIST', count: typeStats['LOGIST']?.count || 0, spheres: typeStats['LOGIST']?.spheres || 0 },
|
||
],
|
||
referralsBySource: [
|
||
{
|
||
source: 'REFERRAL_LINK',
|
||
count: sourceStats['REFERRAL_LINK']?.count || 0,
|
||
spheres: sourceStats['REFERRAL_LINK']?.spheres || 0,
|
||
},
|
||
{
|
||
source: 'AUTO_BUSINESS',
|
||
count: sourceStats['AUTO_BUSINESS']?.count || 0,
|
||
spheres: sourceStats['AUTO_BUSINESS']?.spheres || 0,
|
||
},
|
||
],
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка получения статистики рефералов:', error)
|
||
// Возвращаем заглушку в случае ошибки
|
||
return {
|
||
totalPartners: 0,
|
||
totalSpheres: 0,
|
||
monthlyPartners: 0,
|
||
monthlySpheres: 0,
|
||
referralsByType: [
|
||
{ type: 'SELLER', count: 0, spheres: 0 },
|
||
{ type: 'WHOLESALE', count: 0, spheres: 0 },
|
||
{ type: 'FULFILLMENT', count: 0, spheres: 0 },
|
||
{ type: 'LOGIST', count: 0, spheres: 0 },
|
||
],
|
||
referralsBySource: [
|
||
{ source: 'REFERRAL_LINK', count: 0, spheres: 0 },
|
||
{ source: 'AUTO_BUSINESS', count: 0, spheres: 0 },
|
||
],
|
||
}
|
||
}
|
||
},
|
||
|
||
// Получить список рефералов
|
||
myReferrals: async (_: unknown, args: any, context: Context) => {
|
||
if (!context.user?.organizationId) {
|
||
throw new GraphQLError('Требуется авторизация и организация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const { limit = 50, offset = 0 } = args || {}
|
||
|
||
// Получаем рефералов (организации, которых пригласил текущий пользователь)
|
||
const referralTransactions = await prisma.referralTransaction.findMany({
|
||
where: { referrerId: context.user.organizationId },
|
||
include: {
|
||
referral: {
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
fullName: true,
|
||
inn: true,
|
||
type: true,
|
||
createdAt: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
skip: offset,
|
||
take: limit,
|
||
})
|
||
|
||
// Преобразуем в формат для UI
|
||
const referrals = referralTransactions.map((tx) => ({
|
||
id: tx.id,
|
||
organization: tx.referral,
|
||
source: tx.type === 'REGISTRATION' ? 'REFERRAL_LINK' : 'AUTO_BUSINESS',
|
||
spheresEarned: tx.points,
|
||
registeredAt: tx.createdAt.toISOString(),
|
||
status: 'ACTIVE',
|
||
}))
|
||
|
||
// Получаем общее количество для пагинации
|
||
const totalCount = await prisma.referralTransaction.count({
|
||
where: { referrerId: context.user.organizationId },
|
||
})
|
||
|
||
const totalPages = Math.ceil(totalCount / limit)
|
||
|
||
return {
|
||
referrals,
|
||
totalCount,
|
||
totalPages,
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка получения рефералов:', error)
|
||
return {
|
||
referrals: [],
|
||
totalCount: 0,
|
||
totalPages: 0,
|
||
}
|
||
}
|
||
},
|
||
|
||
// Получить историю транзакций рефералов
|
||
myReferralTransactions: async (_: unknown, args: { limit?: number; offset?: number }, context: Context) => {
|
||
if (!context.user?.organizationId) {
|
||
throw new GraphQLError('Требуется авторизация и организация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Временная заглушка для отладки
|
||
const result = {
|
||
transactions: [],
|
||
totalCount: 0,
|
||
}
|
||
return result
|
||
} catch (error) {
|
||
console.error('Ошибка получения транзакций рефералов:', error)
|
||
return {
|
||
transactions: [],
|
||
totalCount: 0,
|
||
}
|
||
}
|
||
},
|
||
|
||
// 🔒 Мои поставки с системой безопасности (многоуровневая таблица)
|
||
mySupplyOrders: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ
|
||
const securityContext = createSecurityContext({
|
||
user: {
|
||
id: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
},
|
||
req: context.req,
|
||
})
|
||
|
||
console.warn('🔍 GET MY SUPPLY ORDERS (SECURE):', {
|
||
userId: context.user.id,
|
||
organizationType: currentUser.organization.type,
|
||
organizationId: currentUser.organization.id,
|
||
securityEnabled: true,
|
||
})
|
||
|
||
try {
|
||
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
|
||
await ParticipantIsolation.validateAccess(
|
||
prisma,
|
||
currentUser.organization.id,
|
||
currentUser.organization.type,
|
||
'SUPPLY_ORDER',
|
||
)
|
||
|
||
// Определяем логику фильтрации в зависимости от типа организации
|
||
let whereClause
|
||
if (currentUser.organization.type === 'WHOLESALE') {
|
||
// Поставщик видит заказы, где он является поставщиком (partnerId)
|
||
whereClause = {
|
||
partnerId: currentUser.organization.id,
|
||
}
|
||
} else {
|
||
// Остальные (SELLER, FULFILLMENT) видят заказы, которые они создали (organizationId)
|
||
whereClause = {
|
||
organizationId: currentUser.organization.id,
|
||
}
|
||
}
|
||
|
||
const supplyOrders = await prisma.supplyOrder.findMany({
|
||
where: whereClause,
|
||
include: {
|
||
partner: true, // Поставщик (уровень 3)
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
// Товары (уровень 4)
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: {
|
||
createdAt: 'asc',
|
||
},
|
||
},
|
||
},
|
||
orderBy: {
|
||
createdAt: 'desc', // Новые поставки сверху (по номеру)
|
||
},
|
||
})
|
||
|
||
console.warn('📦 Найдено поставок (до фильтрации):', supplyOrders.length, {
|
||
organizationType: currentUser.organization.type,
|
||
filterType: currentUser.organization.type === 'WHOLESALE' ? 'partnerId' : 'organizationId',
|
||
organizationId: currentUser.organization.id,
|
||
})
|
||
|
||
// 🔒 ПРИМЕНЕНИЕ СИСТЕМЫ БЕЗОПАСНОСТИ К КАЖДОМУ ЗАКАЗУ
|
||
const secureProcessedOrders = await Promise.all(
|
||
supplyOrders.map(async (order) => {
|
||
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
|
||
await CommercialDataAudit.logAccess(prisma, {
|
||
userId: currentUser.id,
|
||
organizationType: currentUser.organization.type,
|
||
action: 'VIEW_PRICE',
|
||
resourceType: 'SUPPLY_ORDER',
|
||
resourceId: order.id,
|
||
metadata: {
|
||
orderStatus: order.status,
|
||
totalAmount: order.totalAmount,
|
||
partner: order.partner?.name || order.partner?.inn,
|
||
},
|
||
ipAddress: securityContext.ipAddress,
|
||
userAgent: securityContext.userAgent,
|
||
})
|
||
|
||
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ПО РОЛИ
|
||
const filteredOrder = SupplyDataFilter.filterSupplyOrder(order, securityContext)
|
||
|
||
// Обрабатываем каждый товар для получения рецептуры с фильтрацией
|
||
const processedItems = await Promise.all(
|
||
filteredOrder.data.items.map(async (item: any) => {
|
||
let recipe = null
|
||
|
||
// Получаем развернутую рецептуру если есть данные
|
||
if (
|
||
item.services?.length > 0 ||
|
||
item.fulfillmentConsumables?.length > 0 ||
|
||
item.sellerConsumables?.length > 0
|
||
) {
|
||
// 🔒 АУДИТ ДОСТУПА К РЕЦЕПТУРЕ
|
||
await CommercialDataAudit.logAccess(prisma, {
|
||
userId: currentUser.id,
|
||
organizationType: currentUser.organization.type,
|
||
action: 'VIEW_RECIPE',
|
||
resourceType: 'SUPPLY_ORDER',
|
||
resourceId: item.id,
|
||
metadata: {
|
||
hasServices: item.services?.length > 0,
|
||
hasFulfillmentConsumables: item.fulfillmentConsumables?.length > 0,
|
||
hasSellerConsumables: item.sellerConsumables?.length > 0,
|
||
},
|
||
ipAddress: securityContext.ipAddress,
|
||
userAgent: securityContext.userAgent,
|
||
})
|
||
|
||
// Получаем услуги с фильтрацией
|
||
const services =
|
||
item.services?.length > 0
|
||
? await prisma.service.findMany({
|
||
where: { id: { in: item.services } },
|
||
include: { organization: true },
|
||
})
|
||
: []
|
||
|
||
// Получаем расходники фулфилмента с фильтрацией
|
||
const fulfillmentConsumables =
|
||
item.fulfillmentConsumables?.length > 0
|
||
? await prisma.supply.findMany({
|
||
where: { id: { in: item.fulfillmentConsumables } },
|
||
include: { organization: true },
|
||
})
|
||
: []
|
||
|
||
// Получаем расходники селлера с фильтрацией
|
||
const sellerConsumables =
|
||
item.sellerConsumables?.length > 0
|
||
? await prisma.supply.findMany({
|
||
where: { id: { in: item.sellerConsumables } },
|
||
})
|
||
: []
|
||
|
||
// 🔒 ФИЛЬТРАЦИЯ РЕЦЕПТУРЫ ПО РОЛИ
|
||
// Для WHOLESALE скрываем рецептуру полностью
|
||
if (currentUser.organization.type === 'WHOLESALE') {
|
||
recipe = null
|
||
} else {
|
||
recipe = {
|
||
services,
|
||
fulfillmentConsumables,
|
||
sellerConsumables,
|
||
marketplaceCardId: item.marketplaceCardId,
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
...item,
|
||
price: item.price || 0, // Исправлено: защита от null значения в существующих данных
|
||
recipe,
|
||
}
|
||
}),
|
||
)
|
||
|
||
return {
|
||
...filteredOrder.data,
|
||
items: processedItems,
|
||
// 🔒 ДОБАВЛЯЕМ МЕТАДАННЫЕ БЕЗОПАСНОСТИ
|
||
_security: {
|
||
filtered: filteredOrder.filtered,
|
||
removedFields: filteredOrder.removedFields,
|
||
accessLevel: filteredOrder.accessLevel,
|
||
},
|
||
}
|
||
}),
|
||
)
|
||
|
||
console.warn('✅ Данные обработаны с системой безопасности:', {
|
||
ordersTotal: secureProcessedOrders.length,
|
||
securityApplied: true,
|
||
organizationType: currentUser.organization.type,
|
||
})
|
||
|
||
return secureProcessedOrders
|
||
} catch (error) {
|
||
console.error('❌ Ошибка получения поставок (security):', error)
|
||
throw new GraphQLError(`Ошибка получения поставок: ${error instanceof Error ? error.message : String(error)}`)
|
||
}
|
||
},
|
||
|
||
// Новая система поставок v2
|
||
...fulfillmentConsumableV2Queries,
|
||
},
|
||
|
||
Mutation: {
|
||
sendSmsCode: async (_: unknown, args: { phone: string }) => {
|
||
const result = await smsService.sendSmsCode(args.phone)
|
||
return {
|
||
success: result.success,
|
||
message: result.message || 'SMS код отправлен',
|
||
}
|
||
},
|
||
|
||
verifySmsCode: async (_: unknown, args: { phone: string; code: string }) => {
|
||
const verificationResult = await smsService.verifySmsCode(args.phone, args.code)
|
||
|
||
if (!verificationResult.success) {
|
||
return {
|
||
success: false,
|
||
message: verificationResult.message || 'Неверный код',
|
||
}
|
||
}
|
||
|
||
// Найти или создать пользователя
|
||
const formattedPhone = args.phone.replace(/\D/g, '')
|
||
let user = await prisma.user.findUnique({
|
||
where: { phone: formattedPhone },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user) {
|
||
user = await prisma.user.create({
|
||
data: {
|
||
phone: formattedPhone,
|
||
},
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
}
|
||
|
||
const token = generateToken({
|
||
userId: user.id,
|
||
phone: user.phone,
|
||
})
|
||
|
||
console.warn('verifySmsCode - Generated token:', token ? `${token.substring(0, 20)}...` : 'No token')
|
||
console.warn('verifySmsCode - Full token:', token)
|
||
console.warn('verifySmsCode - User object:', {
|
||
id: user.id,
|
||
phone: user.phone,
|
||
})
|
||
|
||
const result = {
|
||
success: true,
|
||
message: 'Авторизация успешна',
|
||
token,
|
||
user,
|
||
}
|
||
|
||
console.warn('verifySmsCode - Returning result:', {
|
||
success: result.success,
|
||
hasToken: !!result.token,
|
||
hasUser: !!result.user,
|
||
message: result.message,
|
||
tokenPreview: result.token ? `${result.token.substring(0, 20)}...` : 'No token in result',
|
||
})
|
||
|
||
return result
|
||
},
|
||
|
||
verifyInn: async (_: unknown, args: { inn: string }) => {
|
||
// Валидируем ИНН
|
||
if (!dadataService.validateInn(args.inn)) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверный формат ИНН',
|
||
}
|
||
}
|
||
|
||
// Получаем данные организации из DaData
|
||
const organizationData = await dadataService.getOrganizationByInn(args.inn)
|
||
if (!organizationData) {
|
||
return {
|
||
success: false,
|
||
message: 'Организация с указанным ИНН не найдена',
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'ИНН найден',
|
||
organization: {
|
||
name: organizationData.name,
|
||
fullName: organizationData.fullName,
|
||
address: organizationData.address,
|
||
isActive: organizationData.isActive,
|
||
},
|
||
}
|
||
},
|
||
|
||
registerFulfillmentOrganization: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
phone: string
|
||
inn: string
|
||
type: 'FULFILLMENT' | 'LOGIST' | 'WHOLESALE'
|
||
referralCode?: string
|
||
partnerCode?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const { inn, type, referralCode, partnerCode } = args.input
|
||
|
||
// Валидируем ИНН
|
||
if (!dadataService.validateInn(inn)) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверный формат ИНН',
|
||
}
|
||
}
|
||
|
||
// Получаем данные организации из DaData
|
||
const organizationData = await dadataService.getOrganizationByInn(inn)
|
||
if (!organizationData) {
|
||
return {
|
||
success: false,
|
||
message: 'Организация с указанным ИНН не найдена',
|
||
}
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что организация еще не зарегистрирована
|
||
const existingOrg = await prisma.organization.findUnique({
|
||
where: { inn: organizationData.inn },
|
||
})
|
||
|
||
if (existingOrg) {
|
||
return {
|
||
success: false,
|
||
message: 'Организация с таким ИНН уже зарегистрирована',
|
||
}
|
||
}
|
||
|
||
// Генерируем уникальный реферальный код
|
||
const generatedReferralCode = await generateReferralCode()
|
||
|
||
// Создаем организацию со всеми данными из DaData
|
||
const organization = await prisma.organization.create({
|
||
data: {
|
||
inn: organizationData.inn,
|
||
kpp: organizationData.kpp,
|
||
name: organizationData.name,
|
||
fullName: organizationData.fullName,
|
||
address: organizationData.address,
|
||
addressFull: organizationData.addressFull,
|
||
ogrn: organizationData.ogrn,
|
||
ogrnDate: organizationData.ogrnDate,
|
||
|
||
// Статус организации
|
||
status: organizationData.status,
|
||
actualityDate: organizationData.actualityDate,
|
||
registrationDate: organizationData.registrationDate,
|
||
liquidationDate: organizationData.liquidationDate,
|
||
|
||
// Руководитель
|
||
managementName: organizationData.managementName,
|
||
managementPost: organizationData.managementPost,
|
||
|
||
// ОПФ
|
||
opfCode: organizationData.opfCode,
|
||
opfFull: organizationData.opfFull,
|
||
opfShort: organizationData.opfShort,
|
||
|
||
// Коды статистики
|
||
okato: organizationData.okato,
|
||
oktmo: organizationData.oktmo,
|
||
okpo: organizationData.okpo,
|
||
okved: organizationData.okved,
|
||
|
||
// Контакты
|
||
phones: organizationData.phones ? JSON.parse(JSON.stringify(organizationData.phones)) : null,
|
||
emails: organizationData.emails ? JSON.parse(JSON.stringify(organizationData.emails)) : null,
|
||
|
||
// Финансовые данные
|
||
employeeCount: organizationData.employeeCount,
|
||
revenue: organizationData.revenue,
|
||
taxSystem: organizationData.taxSystem,
|
||
|
||
type: type,
|
||
dadataData: JSON.parse(JSON.stringify(organizationData.rawData)),
|
||
|
||
// Реферальная система - генерируем код автоматически
|
||
referralCode: generatedReferralCode,
|
||
},
|
||
})
|
||
|
||
// Привязываем пользователя к организации
|
||
const updatedUser = await prisma.user.update({
|
||
where: { id: context.user.id },
|
||
data: { organizationId: organization.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// Обрабатываем реферальные коды
|
||
if (referralCode) {
|
||
try {
|
||
// Находим реферера по реферальному коду
|
||
const referrer = await prisma.organization.findUnique({
|
||
where: { referralCode: referralCode },
|
||
})
|
||
|
||
if (referrer) {
|
||
// Создаем реферальную транзакцию (100 сфер)
|
||
await prisma.referralTransaction.create({
|
||
data: {
|
||
referrerId: referrer.id,
|
||
referralId: organization.id,
|
||
points: 100,
|
||
type: 'REGISTRATION',
|
||
description: `Регистрация ${type.toLowerCase()} организации по реферальной ссылке`,
|
||
},
|
||
})
|
||
|
||
// Увеличиваем счетчик сфер у реферера
|
||
await prisma.organization.update({
|
||
where: { id: referrer.id },
|
||
data: { referralPoints: { increment: 100 } },
|
||
})
|
||
|
||
// Устанавливаем связь реферала и источник регистрации
|
||
await prisma.organization.update({
|
||
where: { id: organization.id },
|
||
data: { referredById: referrer.id },
|
||
})
|
||
}
|
||
} catch {
|
||
// Error processing referral code, but continue registration
|
||
}
|
||
}
|
||
|
||
if (partnerCode) {
|
||
try {
|
||
// Находим партнера по партнерскому коду
|
||
const partner = await prisma.organization.findUnique({
|
||
where: { referralCode: partnerCode },
|
||
})
|
||
|
||
if (partner) {
|
||
// Создаем реферальную транзакцию (100 сфер)
|
||
await prisma.referralTransaction.create({
|
||
data: {
|
||
referrerId: partner.id,
|
||
referralId: organization.id,
|
||
points: 100,
|
||
type: 'AUTO_PARTNERSHIP',
|
||
description: `Регистрация ${type.toLowerCase()} организации по партнерской ссылке`,
|
||
},
|
||
})
|
||
|
||
// Увеличиваем счетчик сфер у партнера
|
||
await prisma.organization.update({
|
||
where: { id: partner.id },
|
||
data: { referralPoints: { increment: 100 } },
|
||
})
|
||
|
||
// Устанавливаем связь реферала и источник регистрации
|
||
await prisma.organization.update({
|
||
where: { id: organization.id },
|
||
data: { referredById: partner.id },
|
||
})
|
||
|
||
// Создаем партнерскую связь (автоматическое добавление в контрагенты)
|
||
await prisma.counterparty.create({
|
||
data: {
|
||
organizationId: partner.id,
|
||
counterpartyId: organization.id,
|
||
type: 'AUTO',
|
||
triggeredBy: 'PARTNER_LINK',
|
||
},
|
||
})
|
||
|
||
await prisma.counterparty.create({
|
||
data: {
|
||
organizationId: organization.id,
|
||
counterpartyId: partner.id,
|
||
type: 'AUTO',
|
||
triggeredBy: 'PARTNER_LINK',
|
||
},
|
||
})
|
||
}
|
||
} catch {
|
||
// Error processing partner code, but continue registration
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Организация успешно зарегистрирована',
|
||
user: updatedUser,
|
||
}
|
||
} catch {
|
||
// Error registering fulfillment organization
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при регистрации организации',
|
||
}
|
||
}
|
||
},
|
||
|
||
registerSellerOrganization: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
phone: string
|
||
wbApiKey?: string
|
||
ozonApiKey?: string
|
||
ozonClientId?: string
|
||
referralCode?: string
|
||
partnerCode?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const { wbApiKey, ozonApiKey, ozonClientId, referralCode, partnerCode } = args.input
|
||
|
||
if (!wbApiKey && !ozonApiKey) {
|
||
return {
|
||
success: false,
|
||
message: 'Необходимо указать хотя бы один API ключ маркетплейса',
|
||
}
|
||
}
|
||
|
||
try {
|
||
// Валидируем API ключи
|
||
const validationResults = []
|
||
|
||
if (wbApiKey) {
|
||
const wbResult = await marketplaceService.validateWildberriesApiKey(wbApiKey)
|
||
if (!wbResult.isValid) {
|
||
return {
|
||
success: false,
|
||
message: `Wildberries: ${wbResult.message}`,
|
||
}
|
||
}
|
||
validationResults.push({
|
||
marketplace: 'WILDBERRIES',
|
||
apiKey: wbApiKey,
|
||
data: wbResult.data,
|
||
})
|
||
}
|
||
|
||
if (ozonApiKey && ozonClientId) {
|
||
const ozonResult = await marketplaceService.validateOzonApiKey(ozonApiKey, ozonClientId)
|
||
if (!ozonResult.isValid) {
|
||
return {
|
||
success: false,
|
||
message: `Ozon: ${ozonResult.message}`,
|
||
}
|
||
}
|
||
validationResults.push({
|
||
marketplace: 'OZON',
|
||
apiKey: ozonApiKey,
|
||
data: ozonResult.data,
|
||
})
|
||
}
|
||
|
||
// Создаем организацию селлера - используем tradeMark как основное имя
|
||
const tradeMark = validationResults[0]?.data?.tradeMark
|
||
const sellerName = validationResults[0]?.data?.sellerName
|
||
const shopName = tradeMark || sellerName || 'Магазин'
|
||
|
||
// Генерируем уникальный реферальный код
|
||
const generatedReferralCode = await generateReferralCode()
|
||
|
||
const organization = await prisma.organization.create({
|
||
data: {
|
||
inn: (validationResults[0]?.data?.inn as string) || `SELLER_${Date.now()}`,
|
||
name: shopName, // Используем tradeMark как основное название
|
||
fullName: sellerName ? `${sellerName} (${shopName})` : `Интернет-магазин "${shopName}"`,
|
||
type: 'SELLER',
|
||
|
||
// Реферальная система - генерируем код автоматически
|
||
referralCode: generatedReferralCode,
|
||
},
|
||
})
|
||
|
||
// Добавляем API ключи
|
||
for (const validation of validationResults) {
|
||
await prisma.apiKey.create({
|
||
data: {
|
||
marketplace: validation.marketplace as 'WILDBERRIES' | 'OZON',
|
||
apiKey: validation.apiKey,
|
||
organizationId: organization.id,
|
||
validationData: JSON.parse(JSON.stringify(validation.data)),
|
||
},
|
||
})
|
||
}
|
||
|
||
// Привязываем пользователя к организации
|
||
const updatedUser = await prisma.user.update({
|
||
where: { id: context.user.id },
|
||
data: { organizationId: organization.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// Обрабатываем реферальные коды
|
||
if (referralCode) {
|
||
try {
|
||
// Находим реферера по реферальному коду
|
||
const referrer = await prisma.organization.findUnique({
|
||
where: { referralCode: referralCode },
|
||
})
|
||
|
||
if (referrer) {
|
||
// Создаем реферальную транзакцию (100 сфер)
|
||
await prisma.referralTransaction.create({
|
||
data: {
|
||
referrerId: referrer.id,
|
||
referralId: organization.id,
|
||
points: 100,
|
||
type: 'REGISTRATION',
|
||
description: 'Регистрация селлер организации по реферальной ссылке',
|
||
},
|
||
})
|
||
|
||
// Увеличиваем счетчик сфер у реферера
|
||
await prisma.organization.update({
|
||
where: { id: referrer.id },
|
||
data: { referralPoints: { increment: 100 } },
|
||
})
|
||
|
||
// Устанавливаем связь реферала и источник регистрации
|
||
await prisma.organization.update({
|
||
where: { id: organization.id },
|
||
data: { referredById: referrer.id },
|
||
})
|
||
}
|
||
} catch {
|
||
// Error processing referral code, but continue registration
|
||
}
|
||
}
|
||
|
||
if (partnerCode) {
|
||
try {
|
||
// Находим партнера по партнерскому коду
|
||
const partner = await prisma.organization.findUnique({
|
||
where: { referralCode: partnerCode },
|
||
})
|
||
|
||
if (partner) {
|
||
// Создаем реферальную транзакцию (100 сфер)
|
||
await prisma.referralTransaction.create({
|
||
data: {
|
||
referrerId: partner.id,
|
||
referralId: organization.id,
|
||
points: 100,
|
||
type: 'AUTO_PARTNERSHIP',
|
||
description: 'Регистрация селлер организации по партнерской ссылке',
|
||
},
|
||
})
|
||
|
||
// Увеличиваем счетчик сфер у партнера
|
||
await prisma.organization.update({
|
||
where: { id: partner.id },
|
||
data: { referralPoints: { increment: 100 } },
|
||
})
|
||
|
||
// Устанавливаем связь реферала и источник регистрации
|
||
await prisma.organization.update({
|
||
where: { id: organization.id },
|
||
data: { referredById: partner.id },
|
||
})
|
||
|
||
// Создаем партнерскую связь (автоматическое добавление в контрагенты)
|
||
await prisma.counterparty.create({
|
||
data: {
|
||
organizationId: partner.id,
|
||
counterpartyId: organization.id,
|
||
type: 'AUTO',
|
||
triggeredBy: 'PARTNER_LINK',
|
||
},
|
||
})
|
||
|
||
await prisma.counterparty.create({
|
||
data: {
|
||
organizationId: organization.id,
|
||
counterpartyId: partner.id,
|
||
type: 'AUTO',
|
||
triggeredBy: 'PARTNER_LINK',
|
||
},
|
||
})
|
||
}
|
||
} catch {
|
||
// Error processing partner code, but continue registration
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Селлер организация успешно зарегистрирована',
|
||
user: updatedUser,
|
||
}
|
||
} catch {
|
||
// Error registering seller organization
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при регистрации организации',
|
||
}
|
||
}
|
||
},
|
||
|
||
addMarketplaceApiKey: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
marketplace: 'WILDBERRIES' | 'OZON'
|
||
apiKey: string
|
||
clientId?: string
|
||
validateOnly?: boolean
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
// Разрешаем валидацию без авторизации
|
||
if (!args.input.validateOnly && !context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const { marketplace, apiKey, clientId, validateOnly } = args.input
|
||
|
||
console.warn(`🔍 Validating ${marketplace} API key:`, {
|
||
keyLength: apiKey.length,
|
||
keyPreview: apiKey.substring(0, 20) + '...',
|
||
validateOnly,
|
||
})
|
||
|
||
// Валидируем API ключ
|
||
const validationResult = await marketplaceService.validateApiKey(marketplace, apiKey, clientId)
|
||
|
||
console.warn(`✅ Validation result for ${marketplace}:`, validationResult)
|
||
|
||
if (!validationResult.isValid) {
|
||
console.warn(`❌ Validation failed for ${marketplace}:`, validationResult.message)
|
||
return {
|
||
success: false,
|
||
message: validationResult.message,
|
||
}
|
||
}
|
||
|
||
// Если это только валидация, возвращаем результат без сохранения
|
||
if (validateOnly) {
|
||
return {
|
||
success: true,
|
||
message: 'API ключ действителен',
|
||
apiKey: {
|
||
id: 'validate-only',
|
||
marketplace,
|
||
apiKey: '***', // Скрываем реальный ключ при валидации
|
||
isActive: true,
|
||
validationData: validationResult,
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
},
|
||
}
|
||
}
|
||
|
||
// Для сохранения API ключа нужна авторизация
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация для сохранения API ключа', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
return {
|
||
success: false,
|
||
message: 'Пользователь не привязан к организации',
|
||
}
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что такого ключа еще нет
|
||
const existingKey = await prisma.apiKey.findUnique({
|
||
where: {
|
||
organizationId_marketplace: {
|
||
organizationId: user.organization.id,
|
||
marketplace,
|
||
},
|
||
},
|
||
})
|
||
|
||
if (existingKey) {
|
||
// Обновляем существующий ключ
|
||
const updatedKey = await prisma.apiKey.update({
|
||
where: { id: existingKey.id },
|
||
data: {
|
||
apiKey,
|
||
validationData: JSON.parse(JSON.stringify(validationResult.data)),
|
||
isActive: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'API ключ успешно обновлен',
|
||
apiKey: updatedKey,
|
||
}
|
||
} else {
|
||
// Создаем новый ключ
|
||
const newKey = await prisma.apiKey.create({
|
||
data: {
|
||
marketplace,
|
||
apiKey,
|
||
organizationId: user.organization.id,
|
||
validationData: JSON.parse(JSON.stringify(validationResult.data)),
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'API ключ успешно добавлен',
|
||
apiKey: newKey,
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding marketplace API key:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при добавлении API ключа',
|
||
}
|
||
}
|
||
},
|
||
|
||
removeMarketplaceApiKey: async (_: unknown, args: { marketplace: 'WILDBERRIES' | 'OZON' }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Пользователь не привязан к организации')
|
||
}
|
||
|
||
try {
|
||
await prisma.apiKey.delete({
|
||
where: {
|
||
organizationId_marketplace: {
|
||
organizationId: user.organization.id,
|
||
marketplace: args.marketplace,
|
||
},
|
||
},
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error removing marketplace API key:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
updateUserProfile: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
avatar?: string
|
||
orgPhone?: string
|
||
managerName?: string
|
||
telegram?: string
|
||
whatsapp?: string
|
||
email?: string
|
||
bankName?: string
|
||
bik?: string
|
||
accountNumber?: string
|
||
corrAccount?: string
|
||
market?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Пользователь не привязан к организации')
|
||
}
|
||
|
||
try {
|
||
const { input } = args
|
||
|
||
// Обновляем данные пользователя (аватар, имя управляющего)
|
||
const userUpdateData: { avatar?: string; managerName?: string } = {}
|
||
if (input.avatar) {
|
||
userUpdateData.avatar = input.avatar
|
||
}
|
||
if (input.managerName) {
|
||
userUpdateData.managerName = input.managerName
|
||
}
|
||
|
||
if (Object.keys(userUpdateData).length > 0) {
|
||
await prisma.user.update({
|
||
where: { id: context.user.id },
|
||
data: userUpdateData,
|
||
})
|
||
}
|
||
|
||
// Подготавливаем данные для обновления организации
|
||
const updateData: {
|
||
phones?: object
|
||
emails?: object
|
||
managementName?: string
|
||
managementPost?: string
|
||
market?: string
|
||
} = {}
|
||
|
||
// Название организации больше не обновляется через профиль
|
||
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
|
||
|
||
// Обновляем контактные данные в JSON поле phones
|
||
if (input.orgPhone) {
|
||
updateData.phones = [{ value: input.orgPhone, type: 'main' }]
|
||
}
|
||
|
||
// Обновляем email в JSON поле emails
|
||
if (input.email) {
|
||
updateData.emails = [{ value: input.email, type: 'main' }]
|
||
}
|
||
|
||
// Обновляем рынок для поставщиков
|
||
if (input.market !== undefined) {
|
||
updateData.market = input.market === 'none' ? null : input.market
|
||
}
|
||
|
||
// Сохраняем дополнительные контакты в custom полях
|
||
// Пока добавим их как дополнительные JSON поля
|
||
const customContacts: {
|
||
managerName?: string
|
||
telegram?: string
|
||
whatsapp?: string
|
||
bankDetails?: {
|
||
bankName?: string
|
||
bik?: string
|
||
accountNumber?: string
|
||
corrAccount?: string
|
||
}
|
||
} = {}
|
||
|
||
// managerName теперь сохраняется в поле пользователя, а не в JSON
|
||
|
||
if (input.telegram) {
|
||
customContacts.telegram = input.telegram
|
||
}
|
||
|
||
if (input.whatsapp) {
|
||
customContacts.whatsapp = input.whatsapp
|
||
}
|
||
|
||
if (input.bankName || input.bik || input.accountNumber || input.corrAccount) {
|
||
customContacts.bankDetails = {
|
||
bankName: input.bankName,
|
||
bik: input.bik,
|
||
accountNumber: input.accountNumber,
|
||
corrAccount: input.corrAccount,
|
||
}
|
||
}
|
||
|
||
// Если есть дополнительные контакты, сохраним их в поле managementPost временно
|
||
// В идеале нужно добавить отдельную таблицу для контактов
|
||
if (Object.keys(customContacts).length > 0) {
|
||
updateData.managementPost = JSON.stringify(customContacts)
|
||
}
|
||
|
||
// Обновляем организацию
|
||
await prisma.organization.update({
|
||
where: { id: user.organization.id },
|
||
data: updateData,
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
})
|
||
|
||
// Получаем обновленного пользователя
|
||
const updatedUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Профиль успешно обновлен',
|
||
user: updatedUser,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating user profile:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении профиля',
|
||
}
|
||
}
|
||
},
|
||
|
||
updateOrganizationByInn: async (_: unknown, args: { inn: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Пользователь не привязан к организации')
|
||
}
|
||
|
||
try {
|
||
// Валидируем ИНН
|
||
if (!dadataService.validateInn(args.inn)) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверный формат ИНН',
|
||
}
|
||
}
|
||
|
||
// Получаем данные организации из DaData
|
||
const organizationData = await dadataService.getOrganizationByInn(args.inn)
|
||
if (!organizationData) {
|
||
return {
|
||
success: false,
|
||
message: 'Организация с указанным ИНН не найдена в федеральном реестре',
|
||
}
|
||
}
|
||
|
||
// Проверяем, есть ли уже организация с таким ИНН в базе (кроме текущей)
|
||
const existingOrganization = await prisma.organization.findUnique({
|
||
where: { inn: organizationData.inn },
|
||
})
|
||
|
||
if (existingOrganization && existingOrganization.id !== user.organization.id) {
|
||
return {
|
||
success: false,
|
||
message: `Организация с ИНН ${organizationData.inn} уже существует в системе`,
|
||
}
|
||
}
|
||
|
||
// Подготавливаем данные для обновления
|
||
const updateData: Prisma.OrganizationUpdateInput = {
|
||
kpp: organizationData.kpp,
|
||
// Для селлеров не обновляем название организации (это название магазина)
|
||
...(user.organization.type !== 'SELLER' && {
|
||
name: organizationData.name,
|
||
}),
|
||
fullName: organizationData.fullName,
|
||
address: organizationData.address,
|
||
addressFull: organizationData.addressFull,
|
||
ogrn: organizationData.ogrn,
|
||
ogrnDate: organizationData.ogrnDate ? organizationData.ogrnDate.toISOString() : null,
|
||
registrationDate: organizationData.registrationDate ? organizationData.registrationDate.toISOString() : null,
|
||
liquidationDate: organizationData.liquidationDate ? organizationData.liquidationDate.toISOString() : null,
|
||
managementName: organizationData.managementName, // Всегда перезаписываем данными из DaData (может быть null)
|
||
managementPost: user.organization.managementPost, // Сохраняем кастомные данные пользователя
|
||
opfCode: organizationData.opfCode,
|
||
opfFull: organizationData.opfFull,
|
||
opfShort: organizationData.opfShort,
|
||
okato: organizationData.okato,
|
||
oktmo: organizationData.oktmo,
|
||
okpo: organizationData.okpo,
|
||
okved: organizationData.okved,
|
||
status: organizationData.status,
|
||
}
|
||
|
||
// Добавляем ИНН только если он отличается от текущего
|
||
if (user.organization.inn !== organizationData.inn) {
|
||
updateData.inn = organizationData.inn
|
||
}
|
||
|
||
// Обновляем организацию
|
||
await prisma.organization.update({
|
||
where: { id: user.organization.id },
|
||
data: updateData,
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
})
|
||
|
||
// Получаем обновленного пользователя
|
||
const updatedUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Данные организации успешно обновлены',
|
||
user: updatedUser,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating organization by INN:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении данных организации',
|
||
}
|
||
}
|
||
},
|
||
|
||
logout: () => {
|
||
// В stateless JWT системе logout происходит на клиенте
|
||
// Можно добавить blacklist токенов, если нужно
|
||
return true
|
||
},
|
||
|
||
// Отправить заявку на добавление в контрагенты
|
||
sendCounterpartyRequest: async (
|
||
_: unknown,
|
||
args: { organizationId: string; message?: string },
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.id === args.organizationId) {
|
||
throw new GraphQLError('Нельзя отправить заявку самому себе')
|
||
}
|
||
|
||
// Проверяем, что организация-получатель существует
|
||
const receiverOrganization = await prisma.organization.findUnique({
|
||
where: { id: args.organizationId },
|
||
})
|
||
|
||
if (!receiverOrganization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
try {
|
||
// Создаем или обновляем заявку
|
||
const request = await prisma.counterpartyRequest.upsert({
|
||
where: {
|
||
senderId_receiverId: {
|
||
senderId: currentUser.organization.id,
|
||
receiverId: args.organizationId,
|
||
},
|
||
},
|
||
update: {
|
||
status: 'PENDING',
|
||
message: args.message,
|
||
updatedAt: new Date(),
|
||
},
|
||
create: {
|
||
senderId: currentUser.organization.id,
|
||
receiverId: args.organizationId,
|
||
message: args.message,
|
||
status: 'PENDING',
|
||
},
|
||
include: {
|
||
sender: true,
|
||
receiver: true,
|
||
},
|
||
})
|
||
|
||
// Уведомляем получателя о новой заявке
|
||
try {
|
||
notifyOrganization(args.organizationId, {
|
||
type: 'counterparty:request:new',
|
||
payload: {
|
||
requestId: request.id,
|
||
senderId: request.senderId,
|
||
receiverId: request.receiverId,
|
||
},
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Заявка отправлена',
|
||
request,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error sending counterparty request:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отправке заявки',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Ответить на заявку контрагента
|
||
respondToCounterpartyRequest: async (
|
||
_: unknown,
|
||
args: { requestId: string; accept: boolean },
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Найти заявку и проверить права
|
||
const request = await prisma.counterpartyRequest.findUnique({
|
||
where: { id: args.requestId },
|
||
include: {
|
||
sender: true,
|
||
receiver: true,
|
||
},
|
||
})
|
||
|
||
if (!request) {
|
||
throw new GraphQLError('Заявка не найдена')
|
||
}
|
||
|
||
if (request.receiverId !== currentUser.organization.id) {
|
||
throw new GraphQLError('Нет прав на обработку этой заявки')
|
||
}
|
||
|
||
if (request.status !== 'PENDING') {
|
||
throw new GraphQLError('Заявка уже обработана')
|
||
}
|
||
|
||
const newStatus = args.accept ? 'ACCEPTED' : 'REJECTED'
|
||
|
||
// Обновляем статус заявки
|
||
const updatedRequest = await prisma.counterpartyRequest.update({
|
||
where: { id: args.requestId },
|
||
data: { status: newStatus },
|
||
include: {
|
||
sender: true,
|
||
receiver: true,
|
||
},
|
||
})
|
||
|
||
// Если заявка принята, создаем связи контрагентов в обе стороны
|
||
if (args.accept) {
|
||
await prisma.$transaction([
|
||
// Добавляем отправителя в контрагенты получателя
|
||
prisma.counterparty.create({
|
||
data: {
|
||
organizationId: request.receiverId,
|
||
counterpartyId: request.senderId,
|
||
},
|
||
}),
|
||
// Добавляем получателя в контрагенты отправителя
|
||
prisma.counterparty.create({
|
||
data: {
|
||
organizationId: request.senderId,
|
||
counterpartyId: request.receiverId,
|
||
},
|
||
}),
|
||
])
|
||
|
||
// АВТОМАТИЧЕСКОЕ СОЗДАНИЕ ЗАПИСЕЙ В ТАБЛИЦЕ СКЛАДА ФУЛФИЛМЕНТА
|
||
// Проверяем, есть ли фулфилмент среди партнеров
|
||
if (request.receiver.type === 'FULFILLMENT' && request.sender.type === 'SELLER') {
|
||
// Селлер становится партнером фулфилмента - создаем запись склада
|
||
try {
|
||
await autoCreateWarehouseEntry(request.senderId, request.receiverId)
|
||
console.warn(
|
||
`✅ AUTO WAREHOUSE ENTRY: Created for seller ${request.senderId} with fulfillment ${request.receiverId}`,
|
||
)
|
||
} catch (error) {
|
||
console.error('❌ AUTO WAREHOUSE ENTRY ERROR:', error)
|
||
// Не прерываем основной процесс, если не удалось создать запись склада
|
||
}
|
||
} else if (request.sender.type === 'FULFILLMENT' && request.receiver.type === 'SELLER') {
|
||
// Фулфилмент принимает заявку от селлера - создаем запись склада
|
||
try {
|
||
await autoCreateWarehouseEntry(request.receiverId, request.senderId)
|
||
console.warn(
|
||
`✅ AUTO WAREHOUSE ENTRY: Created for seller ${request.receiverId} with fulfillment ${request.senderId}`,
|
||
)
|
||
} catch (error) {
|
||
console.error('❌ AUTO WAREHOUSE ENTRY ERROR:', error)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Оповещаем обе стороны об обновлении заявки и возможном изменении списка контрагентов
|
||
try {
|
||
notifyMany([request.senderId, request.receiverId], {
|
||
type: 'counterparty:request:updated',
|
||
payload: { requestId: updatedRequest.id, status: updatedRequest.status },
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: args.accept ? 'Заявка принята' : 'Заявка отклонена',
|
||
request: updatedRequest,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error responding to counterparty request:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обработке заявки',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Отменить заявку
|
||
cancelCounterpartyRequest: async (_: unknown, args: { requestId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const request = await prisma.counterpartyRequest.findUnique({
|
||
where: { id: args.requestId },
|
||
})
|
||
|
||
if (!request) {
|
||
throw new GraphQLError('Заявка не найдена')
|
||
}
|
||
|
||
if (request.senderId !== currentUser.organization.id) {
|
||
throw new GraphQLError('Можно отменить только свои заявки')
|
||
}
|
||
|
||
if (request.status !== 'PENDING') {
|
||
throw new GraphQLError('Можно отменить только ожидающие заявки')
|
||
}
|
||
|
||
await prisma.counterpartyRequest.update({
|
||
where: { id: args.requestId },
|
||
data: { status: 'CANCELLED' },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error cancelling counterparty request:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Удалить контрагента
|
||
removeCounterparty: async (_: unknown, args: { organizationId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Удаляем связь в обе стороны
|
||
await prisma.$transaction([
|
||
prisma.counterparty.deleteMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterpartyId: args.organizationId,
|
||
},
|
||
}),
|
||
prisma.counterparty.deleteMany({
|
||
where: {
|
||
organizationId: args.organizationId,
|
||
counterpartyId: currentUser.organization.id,
|
||
},
|
||
}),
|
||
])
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error removing counterparty:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Автоматическое создание записи в таблице склада
|
||
autoCreateWarehouseEntry: async (_: unknown, args: { partnerId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что текущая организация - фулфилмент
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Только фулфилмент может создавать записи склада')
|
||
}
|
||
|
||
try {
|
||
// Получаем данные партнера-селлера
|
||
const partnerOrg = await prisma.organization.findUnique({
|
||
where: { id: args.partnerId },
|
||
})
|
||
|
||
if (!partnerOrg) {
|
||
throw new GraphQLError('Партнер не найден')
|
||
}
|
||
|
||
if (partnerOrg.type !== 'SELLER') {
|
||
throw new GraphQLError('Автозаписи создаются только для партнеров-селлеров')
|
||
}
|
||
|
||
// Создаем запись склада
|
||
const warehouseEntry = await autoCreateWarehouseEntry(args.partnerId, currentUser.organization.id)
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Запись склада создана успешно',
|
||
warehouseEntry,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating auto warehouse entry:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка создания записи склада',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Отправить сообщение
|
||
sendMessage: async (
|
||
_: unknown,
|
||
args: {
|
||
receiverOrganizationId: string
|
||
content?: string
|
||
type?: 'TEXT' | 'VOICE'
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что получатель является контрагентом
|
||
const isCounterparty = await prisma.counterparty.findFirst({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterpartyId: args.receiverOrganizationId,
|
||
},
|
||
})
|
||
|
||
if (!isCounterparty) {
|
||
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
|
||
}
|
||
|
||
// Получаем организацию получателя
|
||
const receiverOrganization = await prisma.organization.findUnique({
|
||
where: { id: args.receiverOrganizationId },
|
||
})
|
||
|
||
if (!receiverOrganization) {
|
||
throw new GraphQLError('Организация получателя не найдена')
|
||
}
|
||
|
||
try {
|
||
// Создаем сообщение
|
||
const message = await prisma.message.create({
|
||
data: {
|
||
content: args.content?.trim() || null,
|
||
type: args.type || 'TEXT',
|
||
senderId: context.user.id,
|
||
senderOrganizationId: currentUser.organization.id,
|
||
receiverOrganizationId: args.receiverOrganizationId,
|
||
},
|
||
include: {
|
||
sender: true,
|
||
senderOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
receiverOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// Реалтайм нотификация для обеих организаций (отправитель и получатель)
|
||
try {
|
||
notifyMany([currentUser.organization.id, args.receiverOrganizationId], {
|
||
type: 'message:new',
|
||
payload: {
|
||
messageId: message.id,
|
||
senderOrgId: message.senderOrganizationId,
|
||
receiverOrgId: message.receiverOrganizationId,
|
||
type: message.type,
|
||
},
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Сообщение отправлено',
|
||
messageData: message,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error sending message:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отправке сообщения',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Отправить голосовое сообщение
|
||
sendVoiceMessage: async (
|
||
_: unknown,
|
||
args: {
|
||
receiverOrganizationId: string
|
||
voiceUrl: string
|
||
voiceDuration: number
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что получатель является контрагентом
|
||
const isCounterparty = await prisma.counterparty.findFirst({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterpartyId: args.receiverOrganizationId,
|
||
},
|
||
})
|
||
|
||
if (!isCounterparty) {
|
||
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
|
||
}
|
||
|
||
// Получаем организацию получателя
|
||
const receiverOrganization = await prisma.organization.findUnique({
|
||
where: { id: args.receiverOrganizationId },
|
||
})
|
||
|
||
if (!receiverOrganization) {
|
||
throw new GraphQLError('Организация получателя не найдена')
|
||
}
|
||
|
||
try {
|
||
// Создаем голосовое сообщение
|
||
const message = await prisma.message.create({
|
||
data: {
|
||
content: null,
|
||
type: 'VOICE',
|
||
voiceUrl: args.voiceUrl,
|
||
voiceDuration: args.voiceDuration,
|
||
senderId: context.user.id,
|
||
senderOrganizationId: currentUser.organization.id,
|
||
receiverOrganizationId: args.receiverOrganizationId,
|
||
},
|
||
include: {
|
||
sender: true,
|
||
senderOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
receiverOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
try {
|
||
notifyMany([currentUser.organization.id, args.receiverOrganizationId], {
|
||
type: 'message:new',
|
||
payload: {
|
||
messageId: message.id,
|
||
senderOrgId: message.senderOrganizationId,
|
||
receiverOrgId: message.receiverOrganizationId,
|
||
type: message.type,
|
||
},
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Голосовое сообщение отправлено',
|
||
messageData: message,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error sending voice message:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отправке голосового сообщения',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Отправить изображение
|
||
sendImageMessage: async (
|
||
_: unknown,
|
||
args: {
|
||
receiverOrganizationId: string
|
||
fileUrl: string
|
||
fileName: string
|
||
fileSize: number
|
||
fileType: string
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что получатель является контрагентом
|
||
const isCounterparty = await prisma.counterparty.findFirst({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterpartyId: args.receiverOrganizationId,
|
||
},
|
||
})
|
||
|
||
if (!isCounterparty) {
|
||
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
|
||
}
|
||
|
||
try {
|
||
const message = await prisma.message.create({
|
||
data: {
|
||
content: null,
|
||
type: 'IMAGE',
|
||
fileUrl: args.fileUrl,
|
||
fileName: args.fileName,
|
||
fileSize: args.fileSize,
|
||
fileType: args.fileType,
|
||
senderId: context.user.id,
|
||
senderOrganizationId: currentUser.organization.id,
|
||
receiverOrganizationId: args.receiverOrganizationId,
|
||
},
|
||
include: {
|
||
sender: true,
|
||
senderOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
receiverOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
try {
|
||
notifyMany([currentUser.organization.id, args.receiverOrganizationId], {
|
||
type: 'message:new',
|
||
payload: {
|
||
messageId: message.id,
|
||
senderOrgId: message.senderOrganizationId,
|
||
receiverOrgId: message.receiverOrganizationId,
|
||
type: message.type,
|
||
},
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Изображение отправлено',
|
||
messageData: message,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error sending image:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отправке изображения',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Отправить файл
|
||
sendFileMessage: async (
|
||
_: unknown,
|
||
args: {
|
||
receiverOrganizationId: string
|
||
fileUrl: string
|
||
fileName: string
|
||
fileSize: number
|
||
fileType: string
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что получатель является контрагентом
|
||
const isCounterparty = await prisma.counterparty.findFirst({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterpartyId: args.receiverOrganizationId,
|
||
},
|
||
})
|
||
|
||
if (!isCounterparty) {
|
||
throw new GraphQLError('Можно отправлять сообщения только контрагентам')
|
||
}
|
||
|
||
try {
|
||
const message = await prisma.message.create({
|
||
data: {
|
||
content: null,
|
||
type: 'FILE',
|
||
fileUrl: args.fileUrl,
|
||
fileName: args.fileName,
|
||
fileSize: args.fileSize,
|
||
fileType: args.fileType,
|
||
senderId: context.user.id,
|
||
senderOrganizationId: currentUser.organization.id,
|
||
receiverOrganizationId: args.receiverOrganizationId,
|
||
},
|
||
include: {
|
||
sender: true,
|
||
senderOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
receiverOrganization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
try {
|
||
notifyMany([currentUser.organization.id, args.receiverOrganizationId], {
|
||
type: 'message:new',
|
||
payload: {
|
||
messageId: message.id,
|
||
senderOrgId: message.senderOrganizationId,
|
||
receiverOrgId: message.receiverOrganizationId,
|
||
type: message.type,
|
||
},
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Файл отправлен',
|
||
messageData: message,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error sending file:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отправке файла',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Отметить сообщения как прочитанные
|
||
markMessagesAsRead: async (_: unknown, args: { conversationId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// conversationId имеет формат "currentOrgId-counterpartyId"
|
||
const [, counterpartyId] = args.conversationId.split('-')
|
||
|
||
if (!counterpartyId) {
|
||
throw new GraphQLError('Неверный ID беседы')
|
||
}
|
||
|
||
// Помечаем все непрочитанные сообщения от контрагента как прочитанные
|
||
await prisma.message.updateMany({
|
||
where: {
|
||
senderOrganizationId: counterpartyId,
|
||
receiverOrganizationId: currentUser.organization.id,
|
||
isRead: false,
|
||
},
|
||
data: {
|
||
isRead: true,
|
||
},
|
||
})
|
||
|
||
return true
|
||
},
|
||
|
||
// Создать услугу
|
||
createService: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
name: string
|
||
description?: string
|
||
price: number
|
||
imageUrl?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Услуги доступны только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
const service = await prisma.service.create({
|
||
data: {
|
||
name: args.input.name,
|
||
description: args.input.description,
|
||
price: args.input.price,
|
||
imageUrl: args.input.imageUrl,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: { organization: true },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Услуга успешно создана',
|
||
service,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating service:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании услуги',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить услугу
|
||
updateService: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
input: {
|
||
name: string
|
||
description?: string
|
||
price: number
|
||
imageUrl?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что услуга принадлежит текущей организации
|
||
const existingService = await prisma.service.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingService) {
|
||
throw new GraphQLError('Услуга не найдена или нет доступа')
|
||
}
|
||
|
||
try {
|
||
const service = await prisma.service.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
description: args.input.description,
|
||
price: args.input.price,
|
||
imageUrl: args.input.imageUrl,
|
||
},
|
||
include: { organization: true },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Услуга успешно обновлена',
|
||
service,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating service:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении услуги',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить услугу
|
||
deleteService: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что услуга принадлежит текущей организации
|
||
const existingService = await prisma.service.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingService) {
|
||
throw new GraphQLError('Услуга не найдена или нет доступа')
|
||
}
|
||
|
||
try {
|
||
await prisma.service.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error deleting service:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Обновить цену расходника (новая архитектура - только цену можно редактировать)
|
||
updateSupplyPrice: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
input: {
|
||
pricePerUnit?: number | null
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Обновление цен расходников доступно только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
// Находим и обновляем расходник
|
||
const existingSupply = await prisma.supply.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingSupply) {
|
||
throw new GraphQLError('Расходник не найден')
|
||
}
|
||
|
||
const updatedSupply = await prisma.supply.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
pricePerUnit: args.input.pricePerUnit, // Обновляем цену продажи, НЕ цену закупки
|
||
updatedAt: new Date(),
|
||
},
|
||
include: { organization: true },
|
||
})
|
||
|
||
// Преобразуем в новый формат для GraphQL
|
||
const transformedSupply = {
|
||
id: updatedSupply.id,
|
||
name: updatedSupply.name,
|
||
description: updatedSupply.description,
|
||
pricePerUnit: updatedSupply.price ? parseFloat(updatedSupply.price.toString()) : null, // Конвертируем Decimal в Number
|
||
unit: updatedSupply.unit || 'шт',
|
||
imageUrl: updatedSupply.imageUrl,
|
||
warehouseStock: updatedSupply.currentStock || 0,
|
||
isAvailable: (updatedSupply.currentStock || 0) > 0,
|
||
warehouseConsumableId: updatedSupply.id,
|
||
createdAt: updatedSupply.createdAt,
|
||
updatedAt: updatedSupply.updatedAt,
|
||
organization: updatedSupply.organization,
|
||
}
|
||
|
||
console.warn('🔥 SUPPLY PRICE UPDATED:', {
|
||
id: transformedSupply.id,
|
||
name: transformedSupply.name,
|
||
oldPrice: existingSupply.price,
|
||
newPrice: transformedSupply.pricePerUnit,
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Цена расходника успешно обновлена',
|
||
supply: transformedSupply,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating supply price:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении цены расходника',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Использовать расходники фулфилмента
|
||
useFulfillmentSupplies: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
supplyId: string
|
||
quantityUsed: number
|
||
description?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Использование расходников доступно только для фулфилмент центров')
|
||
}
|
||
|
||
// Находим расходник
|
||
const existingSupply = await prisma.supply.findFirst({
|
||
where: {
|
||
id: args.input.supplyId,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingSupply) {
|
||
throw new GraphQLError('Расходник не найден или нет доступа')
|
||
}
|
||
|
||
// Проверяем, что достаточно расходников
|
||
if (existingSupply.currentStock < args.input.quantityUsed) {
|
||
throw new GraphQLError(
|
||
`Недостаточно расходников. Доступно: ${existingSupply.currentStock}, требуется: ${args.input.quantityUsed}`,
|
||
)
|
||
}
|
||
|
||
try {
|
||
// Обновляем количество расходников
|
||
const updatedSupply = await prisma.supply.update({
|
||
where: { id: args.input.supplyId },
|
||
data: {
|
||
currentStock: existingSupply.currentStock - args.input.quantityUsed,
|
||
updatedAt: new Date(),
|
||
},
|
||
include: { organization: true },
|
||
})
|
||
|
||
console.warn('🔧 Использованы расходники фулфилмента:', {
|
||
supplyName: updatedSupply.name,
|
||
quantityUsed: args.input.quantityUsed,
|
||
remainingStock: updatedSupply.currentStock,
|
||
description: args.input.description,
|
||
})
|
||
|
||
// Реалтайм: уведомляем о смене складских остатков
|
||
try {
|
||
notifyOrganization(currentUser.organization.id, {
|
||
type: 'warehouse:changed',
|
||
payload: { supplyId: updatedSupply.id, change: -args.input.quantityUsed },
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: `Использовано ${args.input.quantityUsed} ${updatedSupply.unit} расходника "${updatedSupply.name}"`,
|
||
supply: updatedSupply,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error using fulfillment supplies:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при использовании расходников',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Создать заказ поставки расходников
|
||
// Два сценария:
|
||
// 1. Селлер → Поставщик → Фулфилмент (селлер заказывает для фулфилмент-центра)
|
||
// 2. Фулфилмент → Поставщик → Фулфилмент (фулфилмент заказывает для себя)
|
||
//
|
||
// Процесс: Заказчик → Поставщик → [Логистика] → Фулфилмент
|
||
// 1. Заказчик (селлер или фулфилмент) создает заказ у поставщика расходников
|
||
// 2. Поставщик получает заказ и готовит товары
|
||
// 3. Логистика транспортирует товары на склад фулфилмента
|
||
// 4. Фулфилмент принимает товары на склад
|
||
// 5. Расходники создаются в системе фулфилмент-центра
|
||
createSupplyOrder: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
partnerId: string
|
||
deliveryDate: string
|
||
fulfillmentCenterId?: string // ID фулфилмент-центра для доставки
|
||
logisticsPartnerId?: string // ID логистической компании
|
||
items: Array<{
|
||
productId: string
|
||
quantity: number
|
||
recipe?: {
|
||
services?: string[]
|
||
fulfillmentConsumables?: string[]
|
||
sellerConsumables?: string[]
|
||
marketplaceCardId?: string
|
||
}
|
||
}>
|
||
notes?: string // Дополнительные заметки к заказу
|
||
consumableType?: string // Классификация расходников
|
||
// Новые поля для многоуровневой системы
|
||
packagesCount?: number // Количество грузовых мест (заполняет поставщик)
|
||
volume?: number // Объём товара в м³ (заполняет поставщик)
|
||
routes?: Array<{
|
||
logisticsId?: string // Ссылка на предустановленный маршрут
|
||
fromLocation: string // Точка забора
|
||
toLocation: string // Точка доставки
|
||
fromAddress?: string // Полный адрес забора
|
||
toAddress?: string // Полный адрес доставки
|
||
}>
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
console.warn('🚀 CREATE_SUPPLY_ORDER RESOLVER - ВЫЗВАН:', {
|
||
hasUser: !!context.user,
|
||
userId: context.user?.id,
|
||
inputData: args.input,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
console.warn('🔍 Проверка пользователя:', {
|
||
userId: context.user.id,
|
||
userFound: !!currentUser,
|
||
organizationFound: !!currentUser?.organization,
|
||
organizationType: currentUser?.organization?.type,
|
||
organizationId: currentUser?.organization?.id,
|
||
})
|
||
|
||
if (!currentUser) {
|
||
throw new GraphQLError('Пользователь не найден')
|
||
}
|
||
|
||
if (!currentUser.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем тип организации и определяем роль в процессе поставки
|
||
const allowedTypes = ['FULFILLMENT', 'SELLER', 'LOGIST']
|
||
if (!allowedTypes.includes(currentUser.organization.type)) {
|
||
throw new GraphQLError('Заказы поставок недоступны для данного типа организации')
|
||
}
|
||
|
||
// Определяем роль организации в процессе поставки
|
||
const organizationRole = currentUser.organization.type
|
||
let fulfillmentCenterId = args.input.fulfillmentCenterId
|
||
|
||
// Если заказ создает фулфилмент-центр, он сам является получателем
|
||
if (organizationRole === 'FULFILLMENT') {
|
||
fulfillmentCenterId = currentUser.organization.id
|
||
}
|
||
|
||
// Если указан фулфилмент-центр, проверяем его существование
|
||
if (fulfillmentCenterId) {
|
||
const fulfillmentCenter = await prisma.organization.findFirst({
|
||
where: {
|
||
id: fulfillmentCenterId,
|
||
type: 'FULFILLMENT',
|
||
},
|
||
})
|
||
|
||
if (!fulfillmentCenter) {
|
||
return {
|
||
success: false,
|
||
message: 'Указанный фулфилмент-центр не найден',
|
||
}
|
||
}
|
||
}
|
||
|
||
// Проверяем, что партнер существует и является поставщиком
|
||
const partner = await prisma.organization.findFirst({
|
||
where: {
|
||
id: args.input.partnerId,
|
||
type: 'WHOLESALE',
|
||
},
|
||
})
|
||
|
||
if (!partner) {
|
||
return {
|
||
success: false,
|
||
message: 'Партнер не найден или не является поставщиком',
|
||
}
|
||
}
|
||
|
||
// Проверяем, что партнер является контрагентом
|
||
const counterparty = await prisma.counterparty.findFirst({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterpartyId: args.input.partnerId,
|
||
},
|
||
})
|
||
|
||
if (!counterparty) {
|
||
return {
|
||
success: false,
|
||
message: 'Данная организация не является вашим партнером',
|
||
}
|
||
}
|
||
|
||
// Получаем товары для проверки наличия и цен
|
||
const productIds = args.input.items.map((item) => item.productId)
|
||
const products = await prisma.product.findMany({
|
||
where: {
|
||
id: { in: productIds },
|
||
organizationId: args.input.partnerId,
|
||
isActive: true,
|
||
},
|
||
})
|
||
|
||
if (products.length !== productIds.length) {
|
||
return {
|
||
success: false,
|
||
message: 'Некоторые товары не найдены или неактивны',
|
||
}
|
||
}
|
||
|
||
// Проверяем наличие товаров
|
||
for (const item of args.input.items) {
|
||
const product = products.find((p) => p.id === item.productId)
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: `Товар ${item.productId} не найден`,
|
||
}
|
||
}
|
||
if (product.quantity < item.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара "${product.name}". Доступно: ${product.quantity}, запрошено: ${item.quantity}`,
|
||
}
|
||
}
|
||
}
|
||
|
||
// Рассчитываем общую сумму и количество
|
||
let totalAmount = 0
|
||
let totalItems = 0
|
||
const orderItems = args.input.items.map((item) => {
|
||
const product = products.find((p) => p.id === item.productId)!
|
||
const itemTotal = Number(product.price) * item.quantity
|
||
totalAmount += itemTotal
|
||
totalItems += item.quantity
|
||
|
||
/* ОТКАТ: Новая логика сохранения рецептур - ЗАКОММЕНТИРОВАНО
|
||
// Получаем полные данные рецептуры из БД
|
||
let recipeData = null
|
||
if (item.recipe && (item.recipe.services?.length || item.recipe.fulfillmentConsumables?.length || item.recipe.sellerConsumables?.length)) {
|
||
// Получаем услуги фулфилмента
|
||
const services = item.recipe.services ? await context.prisma.supply.findMany({
|
||
where: { id: { in: item.recipe.services } },
|
||
select: { id: true, name: true, description: true, pricePerUnit: true }
|
||
}) : []
|
||
|
||
// Получаем расходники фулфилмента
|
||
const fulfillmentConsumables = item.recipe.fulfillmentConsumables ? await context.prisma.supply.findMany({
|
||
where: { id: { in: item.recipe.fulfillmentConsumables } },
|
||
select: { id: true, name: true, description: true, pricePerUnit: true, unit: true, imageUrl: true }
|
||
}) : []
|
||
|
||
// Получаем расходники селлера
|
||
const sellerConsumables = item.recipe.sellerConsumables ? await context.prisma.supply.findMany({
|
||
where: { id: { in: item.recipe.sellerConsumables } },
|
||
select: { id: true, name: true, description: true, pricePerUnit: true, unit: true }
|
||
}) : []
|
||
|
||
recipeData = {
|
||
services: services.map(service => ({
|
||
id: service.id,
|
||
name: service.name,
|
||
description: service.description,
|
||
price: service.pricePerUnit
|
||
})),
|
||
fulfillmentConsumables: fulfillmentConsumables.map(consumable => ({
|
||
id: consumable.id,
|
||
name: consumable.name,
|
||
description: consumable.description,
|
||
price: consumable.pricePerUnit,
|
||
unit: consumable.unit,
|
||
imageUrl: consumable.imageUrl
|
||
})),
|
||
sellerConsumables: sellerConsumables.map(consumable => ({
|
||
id: consumable.id,
|
||
name: consumable.name,
|
||
description: consumable.description,
|
||
price: consumable.pricePerUnit,
|
||
unit: consumable.unit
|
||
})),
|
||
marketplaceCardId: item.recipe.marketplaceCardId
|
||
}
|
||
}
|
||
|
||
return {
|
||
productId: item.productId,
|
||
quantity: item.quantity,
|
||
price: product.price,
|
||
totalPrice: new Prisma.Decimal(itemTotal),
|
||
// Сохраняем полную рецептуру как JSON
|
||
recipe: recipeData ? JSON.stringify(recipeData) : null,
|
||
}
|
||
*/
|
||
|
||
// ВОССТАНОВЛЕННАЯ ОРИГИНАЛЬНАЯ ЛОГИКА:
|
||
return {
|
||
productId: item.productId,
|
||
quantity: item.quantity,
|
||
price: product.price || 0, // Исправлено: защита от null значения
|
||
totalPrice: new Prisma.Decimal(itemTotal),
|
||
// Извлечение данных рецептуры из объекта recipe
|
||
services: item.recipe?.services || [],
|
||
fulfillmentConsumables: item.recipe?.fulfillmentConsumables || [],
|
||
sellerConsumables: item.recipe?.sellerConsumables || [],
|
||
marketplaceCardId: item.recipe?.marketplaceCardId,
|
||
}
|
||
})
|
||
|
||
try {
|
||
// Определяем начальный статус в зависимости от роли организации
|
||
let initialStatus: 'PENDING' | 'CONFIRMED' = 'PENDING'
|
||
if (organizationRole === 'SELLER') {
|
||
initialStatus = 'PENDING' // Селлер создает заказ, ждет подтверждения поставщика
|
||
} else if (organizationRole === 'FULFILLMENT') {
|
||
initialStatus = 'PENDING' // Фулфилмент заказывает для своего склада
|
||
} else if (organizationRole === 'LOGIST') {
|
||
initialStatus = 'CONFIRMED' // Логист может сразу подтверждать заказы
|
||
}
|
||
|
||
// ИСПРАВЛЕНИЕ: Автоматически определяем тип расходников на основе заказчика
|
||
const consumableType =
|
||
currentUser.organization.type === 'SELLER' ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES'
|
||
|
||
console.warn('🔍 Автоматическое определение типа расходников:', {
|
||
organizationType: currentUser.organization.type,
|
||
consumableType: consumableType,
|
||
inputType: args.input.consumableType, // Для отладки
|
||
})
|
||
|
||
// Подготавливаем данные для создания заказа
|
||
const createData: any = {
|
||
partnerId: args.input.partnerId,
|
||
deliveryDate: new Date(args.input.deliveryDate),
|
||
totalAmount: new Prisma.Decimal(totalAmount),
|
||
totalItems: totalItems,
|
||
organizationId: currentUser.organization.id,
|
||
fulfillmentCenterId: fulfillmentCenterId,
|
||
consumableType: consumableType, // ИСПРАВЛЕНО: используем автоматически определенный тип
|
||
status: initialStatus,
|
||
// Новые поля для многоуровневой системы (пока что селлер не может задать эти поля)
|
||
// packagesCount: args.input.packagesCount || null, // Поле не существует в модели
|
||
// volume: args.input.volume || null, // Поле не существует в модели
|
||
// notes: args.input.notes || null, // Поле не существует в модели
|
||
items: {
|
||
create: orderItems,
|
||
},
|
||
}
|
||
|
||
// 🔄 ЛОГИСТИКА ОПЦИОНАЛЬНА: добавляем только если передана
|
||
if (args.input.logisticsPartnerId) {
|
||
createData.logisticsPartnerId = args.input.logisticsPartnerId
|
||
}
|
||
|
||
console.warn('🔍 Создаем SupplyOrder с данными:', {
|
||
hasLogistics: !!args.input.logisticsPartnerId,
|
||
logisticsId: args.input.logisticsPartnerId,
|
||
createData: createData,
|
||
})
|
||
|
||
const supplyOrder = await prisma.supplyOrder.create({
|
||
data: createData,
|
||
include: {
|
||
partner: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
fulfillmentCenter: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
logisticsPartner: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
// employee: true, // Поле не существует в модели
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
// Маршруты будут добавлены отдельно после создания
|
||
},
|
||
})
|
||
|
||
// 📍 СОЗДАЕМ МАРШРУТЫ ПОСТАВКИ (если указаны)
|
||
if (args.input.routes && args.input.routes.length > 0) {
|
||
const routesData = args.input.routes.map((route) => ({
|
||
supplyOrderId: supplyOrder.id,
|
||
logisticsId: route.logisticsId || null,
|
||
fromLocation: route.fromLocation,
|
||
toLocation: route.toLocation,
|
||
fromAddress: route.fromAddress || null,
|
||
toAddress: route.toAddress || null,
|
||
status: 'pending',
|
||
createdDate: new Date(), // Дата создания маршрута (уровень 2)
|
||
}))
|
||
|
||
await prisma.supplyRoute.createMany({
|
||
data: routesData,
|
||
})
|
||
|
||
console.warn(`📍 Созданы маршруты для заказа ${supplyOrder.id}:`, routesData.length)
|
||
} else {
|
||
// Создаем маршрут по умолчанию на основе адресов организаций
|
||
const defaultRoute = {
|
||
supplyOrderId: supplyOrder.id,
|
||
fromLocation: partner.market || partner.address || 'Поставщик',
|
||
toLocation: fulfillmentCenterId ? 'Фулфилмент-центр' : 'Получатель',
|
||
fromAddress: partner.addressFull || partner.address || null,
|
||
toAddress: fulfillmentCenterId
|
||
? (
|
||
await prisma.organization.findUnique({
|
||
where: { id: fulfillmentCenterId },
|
||
select: { addressFull: true, address: true },
|
||
})
|
||
)?.addressFull || null
|
||
: null,
|
||
status: 'pending',
|
||
createdDate: new Date(),
|
||
}
|
||
|
||
await prisma.supplyRoute.create({
|
||
data: defaultRoute,
|
||
})
|
||
|
||
console.warn(`📍 Создан маршрут по умолчанию для заказа ${supplyOrder.id}`)
|
||
}
|
||
|
||
// Реалтайм: уведомляем поставщика и вовлеченные стороны о новом заказе
|
||
try {
|
||
const orgIds = [
|
||
currentUser.organization.id,
|
||
args.input.partnerId,
|
||
fulfillmentCenterId || undefined,
|
||
args.input.logisticsPartnerId || undefined,
|
||
].filter(Boolean) as string[]
|
||
notifyMany(orgIds, {
|
||
type: 'supply-order:new',
|
||
payload: { id: supplyOrder.id, organizationId: currentUser.organization.id },
|
||
})
|
||
} catch {}
|
||
|
||
// 📦 РЕЗЕРВИРУЕМ ТОВАРЫ У ПОСТАВЩИКА
|
||
// Увеличиваем поле "ordered" для каждого заказанного товара
|
||
for (const item of args.input.items) {
|
||
await prisma.product.update({
|
||
where: { id: item.productId },
|
||
data: {
|
||
ordered: {
|
||
increment: item.quantity,
|
||
},
|
||
},
|
||
})
|
||
}
|
||
|
||
console.warn(
|
||
`📦 Зарезервированы товары для заказа ${supplyOrder.id}:`,
|
||
args.input.items.map((item) => `${item.productId}: +${item.quantity} шт.`).join(', '),
|
||
)
|
||
|
||
// Проверяем, является ли это первой сделкой организации
|
||
const isFirstOrder =
|
||
(await prisma.supplyOrder.count({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
id: { not: supplyOrder.id },
|
||
},
|
||
})) === 0
|
||
|
||
// Если это первая сделка и организация была приглашена по реферальной ссылке
|
||
if (isFirstOrder && currentUser.organization.referredById) {
|
||
try {
|
||
// Создаем транзакцию на 100 сфер за первую сделку
|
||
await prisma.referralTransaction.create({
|
||
data: {
|
||
referrerId: currentUser.organization.referredById,
|
||
referralId: currentUser.organization.id,
|
||
points: 100,
|
||
type: 'FIRST_ORDER',
|
||
description: `Первая сделка реферала ${currentUser.organization.name || currentUser.organization.inn}`,
|
||
},
|
||
})
|
||
|
||
// Увеличиваем счетчик сфер у реферера
|
||
await prisma.organization.update({
|
||
where: { id: currentUser.organization.referredById },
|
||
data: { referralPoints: { increment: 100 } },
|
||
})
|
||
|
||
console.log(`💰 Начислено 100 сфер рефереру за первую сделку организации ${currentUser.organization.id}`)
|
||
} catch (error) {
|
||
console.error('Ошибка начисления сфер за первую сделку:', error)
|
||
// Не прерываем создание заказа из-за ошибки начисления
|
||
}
|
||
}
|
||
|
||
// Создаем расходники на основе заказанных товаров
|
||
// Расходники создаются в организации получателя (фулфилмент-центре)
|
||
// Определяем тип расходников на основе consumableType
|
||
const supplyType =
|
||
args.input.consumableType === 'SELLER_CONSUMABLES' ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES'
|
||
|
||
// Определяем sellerOwnerId для расходников селлеров
|
||
const sellerOwnerId = supplyType === 'SELLER_CONSUMABLES' ? currentUser.organization!.id : null
|
||
|
||
const suppliesData = args.input.items.map((item) => {
|
||
const product = products.find((p) => p.id === item.productId)!
|
||
const productWithCategory = supplyOrder.items.find(
|
||
(orderItem: { productId: string; product: { category?: { name: string } | null } }) =>
|
||
orderItem.productId === item.productId,
|
||
)?.product
|
||
|
||
return {
|
||
name: product.name,
|
||
article: product.article, // ИСПРАВЛЕНО: Добавляем артикул товара для уникальности
|
||
description: product.description || `Заказано у ${partner.name}`,
|
||
price: product.price, // Цена закупки у поставщика
|
||
quantity: item.quantity,
|
||
unit: 'шт',
|
||
category: productWithCategory?.category?.name || 'Расходники',
|
||
status: 'planned', // Статус "запланировано" (ожидает одобрения поставщиком)
|
||
date: new Date(args.input.deliveryDate),
|
||
supplier: partner.name || partner.fullName || 'Не указан',
|
||
minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток
|
||
currentStock: 0, // Пока товар не пришел
|
||
type: supplyType, // ИСПРАВЛЕНО: Добавляем тип расходников
|
||
sellerOwnerId: sellerOwnerId, // ИСПРАВЛЕНО: Добавляем владельца для расходников селлеров
|
||
// Расходники создаются в организации получателя (фулфилмент-центре)
|
||
organizationId: fulfillmentCenterId || currentUser.organization!.id,
|
||
}
|
||
})
|
||
|
||
// Создаем расходники
|
||
await prisma.supply.createMany({
|
||
data: suppliesData,
|
||
})
|
||
|
||
// 🔔 ОТПРАВЛЯЕМ УВЕДОМЛЕНИЕ ПОСТАВЩИКУ О НОВОМ ЗАКАЗЕ
|
||
try {
|
||
const orderSummary = args.input.items
|
||
.map((item) => {
|
||
const product = products.find((p) => p.id === item.productId)!
|
||
return `${product.name} - ${item.quantity} шт.`
|
||
})
|
||
.join(', ')
|
||
|
||
const notificationMessage = `🔔 Новый заказ поставки от ${
|
||
currentUser.organization.name || currentUser.organization.fullName
|
||
}!\n\nТовары: ${orderSummary}\nДата доставки: ${new Date(args.input.deliveryDate).toLocaleDateString(
|
||
'ru-RU',
|
||
)}\nОбщая сумма: ${totalAmount.toLocaleString(
|
||
'ru-RU',
|
||
)} ₽\n\nПожалуйста, подтвердите заказ в разделе "Поставки".`
|
||
|
||
await prisma.message.create({
|
||
data: {
|
||
content: notificationMessage,
|
||
type: 'TEXT',
|
||
senderId: context.user.id,
|
||
senderOrganizationId: currentUser.organization.id,
|
||
receiverOrganizationId: args.input.partnerId,
|
||
},
|
||
})
|
||
|
||
console.warn(`✅ Уведомление отправлено поставщику ${partner.name}`)
|
||
} catch (notificationError) {
|
||
console.error('❌ Ошибка отправки уведомления:', notificationError)
|
||
// Не прерываем выполнение, если уведомление не отправилось
|
||
}
|
||
|
||
// Получаем полные данные заказа с маршрутами для ответа
|
||
const completeOrder = await prisma.supplyOrder.findUnique({
|
||
where: { id: supplyOrder.id },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
employee: true,
|
||
routes: {
|
||
include: {
|
||
logistics: true,
|
||
},
|
||
},
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// Формируем сообщение в зависимости от роли организации
|
||
let successMessage = ''
|
||
if (organizationRole === 'SELLER') {
|
||
successMessage = `Заказ поставки товаров создан! Товары будут доставлены ${
|
||
fulfillmentCenterId ? 'на указанный фулфилмент-центр' : 'согласно настройкам'
|
||
}. Ожидайте подтверждения от поставщика.`
|
||
} else if (organizationRole === 'FULFILLMENT') {
|
||
successMessage =
|
||
'Заказ поставки товаров создан для вашего склада! Ожидайте подтверждения от поставщика и координации с логистикой.'
|
||
} else if (organizationRole === 'LOGIST') {
|
||
successMessage =
|
||
'Заказ поставки создан и подтвержден! Координируйте доставку товаров от поставщика на фулфилмент-склад.'
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: successMessage,
|
||
order: completeOrder,
|
||
processInfo: {
|
||
role: organizationRole,
|
||
supplier: partner.name || partner.fullName,
|
||
fulfillmentCenter: fulfillmentCenterId,
|
||
logistics: args.input.logisticsPartnerId,
|
||
status: initialStatus,
|
||
},
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating supply order:', error)
|
||
console.error('ДЕТАЛИ ОШИБКИ:', error instanceof Error ? error.message : String(error))
|
||
console.error('СТЕК ОШИБКИ:', error instanceof Error ? error.stack : 'No stack')
|
||
return {
|
||
success: false,
|
||
message: `Ошибка при создании заказа поставки: ${error instanceof Error ? error.message : String(error)}`,
|
||
}
|
||
}
|
||
},
|
||
|
||
// Создать товар
|
||
createProduct: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
name: string
|
||
article: string
|
||
description?: string
|
||
price: number
|
||
pricePerSet?: number
|
||
quantity: number
|
||
setQuantity?: number
|
||
ordered?: number
|
||
inTransit?: number
|
||
stock?: number
|
||
sold?: number
|
||
type?: 'PRODUCT' | 'CONSUMABLE'
|
||
categoryId?: string
|
||
brand?: string
|
||
color?: string
|
||
size?: string
|
||
weight?: number
|
||
dimensions?: string
|
||
material?: string
|
||
images?: string[]
|
||
mainImage?: string
|
||
isActive?: boolean
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
console.warn('🆕 CREATE_PRODUCT RESOLVER - ВЫЗВАН:', {
|
||
hasUser: !!context.user,
|
||
userId: context.user?.id,
|
||
inputData: args.input,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это поставщик
|
||
if (currentUser.organization.type !== 'WHOLESALE') {
|
||
throw new GraphQLError('Товары доступны только для поставщиков')
|
||
}
|
||
|
||
// Проверяем уникальность артикула в рамках организации
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
article: args.input.article,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (existingProduct) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар с таким артикулом уже существует',
|
||
}
|
||
}
|
||
|
||
try {
|
||
console.warn('🛍️ СОЗДАНИЕ ТОВАРА - НАЧАЛО:', {
|
||
userId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
productData: {
|
||
name: args.input.name,
|
||
article: args.input.article,
|
||
type: args.input.type || 'PRODUCT',
|
||
isActive: args.input.isActive ?? true,
|
||
},
|
||
})
|
||
|
||
const product = await prisma.product.create({
|
||
data: {
|
||
name: args.input.name,
|
||
article: args.input.article,
|
||
description: args.input.description,
|
||
price: args.input.price,
|
||
pricePerSet: args.input.pricePerSet,
|
||
quantity: args.input.quantity,
|
||
setQuantity: args.input.setQuantity,
|
||
ordered: args.input.ordered,
|
||
inTransit: args.input.inTransit,
|
||
stock: args.input.stock,
|
||
sold: args.input.sold,
|
||
type: args.input.type || 'PRODUCT',
|
||
categoryId: args.input.categoryId,
|
||
brand: args.input.brand,
|
||
color: args.input.color,
|
||
size: args.input.size,
|
||
weight: args.input.weight,
|
||
dimensions: args.input.dimensions,
|
||
material: args.input.material,
|
||
images: JSON.stringify(args.input.images || []),
|
||
mainImage: args.input.mainImage,
|
||
isActive: args.input.isActive ?? true,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
console.warn('✅ ТОВАР УСПЕШНО СОЗДАН:', {
|
||
productId: product.id,
|
||
name: product.name,
|
||
article: product.article,
|
||
type: product.type,
|
||
isActive: product.isActive,
|
||
organizationId: product.organizationId,
|
||
createdAt: product.createdAt,
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар успешно создан',
|
||
product,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating product:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании товара',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить товар
|
||
updateProduct: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
input: {
|
||
name: string
|
||
article: string
|
||
description?: string
|
||
price: number
|
||
pricePerSet?: number
|
||
quantity: number
|
||
setQuantity?: number
|
||
ordered?: number
|
||
inTransit?: number
|
||
stock?: number
|
||
sold?: number
|
||
type?: 'PRODUCT' | 'CONSUMABLE'
|
||
categoryId?: string
|
||
brand?: string
|
||
color?: string
|
||
size?: string
|
||
weight?: number
|
||
dimensions?: string
|
||
material?: string
|
||
images?: string[]
|
||
mainImage?: string
|
||
isActive?: boolean
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что товар принадлежит текущей организации
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingProduct) {
|
||
throw new GraphQLError('Товар не найден или нет доступа')
|
||
}
|
||
|
||
// Проверяем уникальность артикула (если он изменился)
|
||
if (args.input.article !== existingProduct.article) {
|
||
const duplicateProduct = await prisma.product.findFirst({
|
||
where: {
|
||
article: args.input.article,
|
||
organizationId: currentUser.organization.id,
|
||
NOT: { id: args.id },
|
||
},
|
||
})
|
||
|
||
if (duplicateProduct) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар с таким артикулом уже существует',
|
||
}
|
||
}
|
||
}
|
||
|
||
try {
|
||
const product = await prisma.product.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
article: args.input.article,
|
||
description: args.input.description,
|
||
price: args.input.price,
|
||
pricePerSet: args.input.pricePerSet,
|
||
quantity: args.input.quantity,
|
||
setQuantity: args.input.setQuantity,
|
||
ordered: args.input.ordered,
|
||
inTransit: args.input.inTransit,
|
||
stock: args.input.stock,
|
||
sold: args.input.sold,
|
||
...(args.input.type && { type: args.input.type }),
|
||
categoryId: args.input.categoryId,
|
||
brand: args.input.brand,
|
||
color: args.input.color,
|
||
size: args.input.size,
|
||
weight: args.input.weight,
|
||
dimensions: args.input.dimensions,
|
||
material: args.input.material,
|
||
images: args.input.images ? JSON.stringify(args.input.images) : undefined,
|
||
mainImage: args.input.mainImage,
|
||
isActive: args.input.isActive ?? true,
|
||
},
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар успешно обновлен',
|
||
product,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating product:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении товара',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Проверка уникальности артикула
|
||
checkArticleUniqueness: async (_: unknown, args: { article: string; excludeId?: string }, context: Context) => {
|
||
const { currentUser, prisma } = context
|
||
|
||
if (!currentUser?.organization?.id) {
|
||
return {
|
||
isUnique: false,
|
||
existingProduct: null,
|
||
}
|
||
}
|
||
|
||
try {
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
article: args.article,
|
||
organizationId: currentUser.organization.id,
|
||
...(args.excludeId && { id: { not: args.excludeId } }),
|
||
},
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
article: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
isUnique: !existingProduct,
|
||
existingProduct,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error checking article uniqueness:', error)
|
||
return {
|
||
isUnique: false,
|
||
existingProduct: null,
|
||
}
|
||
}
|
||
},
|
||
|
||
// Резервирование товара при создании заказа
|
||
reserveProductStock: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
|
||
const { currentUser, prisma } = context
|
||
|
||
if (!currentUser?.organization?.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Необходимо авторизоваться',
|
||
}
|
||
}
|
||
|
||
try {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: args.productId },
|
||
})
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден',
|
||
}
|
||
}
|
||
|
||
// Проверяем доступность товара
|
||
const availableStock = (product.stock || product.quantity) - (product.ordered || 0)
|
||
if (availableStock < args.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара на складе. Доступно: ${availableStock}, запрошено: ${args.quantity}`,
|
||
}
|
||
}
|
||
|
||
// Резервируем товар (увеличиваем поле ordered)
|
||
const updatedProduct = await prisma.product.update({
|
||
where: { id: args.productId },
|
||
data: {
|
||
ordered: (product.ordered || 0) + args.quantity,
|
||
},
|
||
})
|
||
|
||
console.warn(`📦 Зарезервировано ${args.quantity} единиц товара ${product.name}`)
|
||
|
||
return {
|
||
success: true,
|
||
message: `Зарезервировано ${args.quantity} единиц товара`,
|
||
product: updatedProduct,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error reserving product stock:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при резервировании товара',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Освобождение резерва при отмене заказа
|
||
releaseProductReserve: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
|
||
const { currentUser, prisma } = context
|
||
|
||
if (!currentUser?.organization?.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Необходимо авторизоваться',
|
||
}
|
||
}
|
||
|
||
try {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: args.productId },
|
||
})
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден',
|
||
}
|
||
}
|
||
|
||
// Освобождаем резерв (уменьшаем поле ordered)
|
||
const newOrdered = Math.max((product.ordered || 0) - args.quantity, 0)
|
||
|
||
const updatedProduct = await prisma.product.update({
|
||
where: { id: args.productId },
|
||
data: {
|
||
ordered: newOrdered,
|
||
},
|
||
})
|
||
|
||
console.warn(`🔄 Освобожден резерв ${args.quantity} единиц товара ${product.name}`)
|
||
|
||
return {
|
||
success: true,
|
||
message: `Освобожден резерв ${args.quantity} единиц товара`,
|
||
product: updatedProduct,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error releasing product reserve:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при освобождении резерва',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновление статуса "в пути"
|
||
updateProductInTransit: async (
|
||
_: unknown,
|
||
args: { productId: string; quantity: number; operation: string },
|
||
context: Context,
|
||
) => {
|
||
const { currentUser, prisma } = context
|
||
|
||
if (!currentUser?.organization?.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Необходимо авторизоваться',
|
||
}
|
||
}
|
||
|
||
try {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: args.productId },
|
||
})
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден',
|
||
}
|
||
}
|
||
|
||
let newInTransit = product.inTransit || 0
|
||
let newOrdered = product.ordered || 0
|
||
|
||
if (args.operation === 'ship') {
|
||
// При отгрузке: переводим из "заказано" в "в пути"
|
||
newInTransit = (product.inTransit || 0) + args.quantity
|
||
newOrdered = Math.max((product.ordered || 0) - args.quantity, 0)
|
||
} else if (args.operation === 'deliver') {
|
||
// При доставке: убираем из "в пути", добавляем в "продано"
|
||
newInTransit = Math.max((product.inTransit || 0) - args.quantity, 0)
|
||
}
|
||
|
||
const updatedProduct = await prisma.product.update({
|
||
where: { id: args.productId },
|
||
data: {
|
||
inTransit: newInTransit,
|
||
ordered: newOrdered,
|
||
...(args.operation === 'deliver' && {
|
||
sold: (product.sold || 0) + args.quantity,
|
||
}),
|
||
},
|
||
})
|
||
|
||
console.warn(`🚚 Обновлен статус "в пути" для товара ${product.name}: ${args.operation}`)
|
||
|
||
return {
|
||
success: true,
|
||
message: `Статус товара обновлен: ${args.operation}`,
|
||
product: updatedProduct,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating product in transit:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении статуса товара',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить товар
|
||
deleteProduct: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что товар принадлежит текущей организации
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingProduct) {
|
||
throw new GraphQLError('Товар не найден или нет доступа')
|
||
}
|
||
|
||
try {
|
||
await prisma.product.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error deleting product:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Создать категорию
|
||
createCategory: async (_: unknown, args: { input: { name: string } }, context: Context) => {
|
||
if (!context.user && !context.admin) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
// Проверяем уникальность названия категории
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { name: args.input.name },
|
||
})
|
||
|
||
if (existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория с таким названием уже существует',
|
||
}
|
||
}
|
||
|
||
try {
|
||
const category = await prisma.category.create({
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Категория успешно создана',
|
||
category,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating category:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании категории',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить категорию
|
||
updateCategory: async (_: unknown, args: { id: string; input: { name: string } }, context: Context) => {
|
||
if (!context.user && !context.admin) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
// Проверяем существование категории
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
if (!existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория не найдена',
|
||
}
|
||
}
|
||
|
||
// Проверяем уникальность нового названия (если изменилось)
|
||
if (args.input.name !== existingCategory.name) {
|
||
const duplicateCategory = await prisma.category.findUnique({
|
||
where: { name: args.input.name },
|
||
})
|
||
|
||
if (duplicateCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория с таким названием уже существует',
|
||
}
|
||
}
|
||
}
|
||
|
||
try {
|
||
const category = await prisma.category.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Категория успешно обновлена',
|
||
category,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating category:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении категории',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить категорию
|
||
deleteCategory: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user && !context.admin) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
// Проверяем существование категории
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
include: { products: true },
|
||
})
|
||
|
||
if (!existingCategory) {
|
||
throw new GraphQLError('Категория не найдена')
|
||
}
|
||
|
||
// Проверяем, есть ли товары в этой категории
|
||
if (existingCategory.products.length > 0) {
|
||
throw new GraphQLError('Нельзя удалить категорию, в которой есть товары')
|
||
}
|
||
|
||
try {
|
||
await prisma.category.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error deleting category:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Добавить товар в корзину
|
||
addToCart: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что товар существует и активен
|
||
const product = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.productId,
|
||
isActive: true,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден или неактивен',
|
||
}
|
||
}
|
||
|
||
// Проверяем, что пользователь не пытается добавить свой собственный товар
|
||
if (product.organizationId === currentUser.organization.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Нельзя добавлять собственные товары в корзину',
|
||
}
|
||
}
|
||
|
||
// Найти или создать корзину
|
||
let cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
})
|
||
|
||
if (!cart) {
|
||
cart = await prisma.cart.create({
|
||
data: {
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Проверяем, есть ли уже такой товар в корзине
|
||
const existingCartItem = await prisma.cartItem.findUnique({
|
||
where: {
|
||
cartId_productId: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
})
|
||
|
||
if (existingCartItem) {
|
||
// Обновляем количество
|
||
const newQuantity = existingCartItem.quantity + args.quantity
|
||
|
||
if (newQuantity > product.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
|
||
}
|
||
}
|
||
|
||
await prisma.cartItem.update({
|
||
where: { id: existingCartItem.id },
|
||
data: { quantity: newQuantity },
|
||
})
|
||
} else {
|
||
// Создаем новый элемент корзины
|
||
if (args.quantity > product.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
|
||
}
|
||
}
|
||
|
||
await prisma.cartItem.create({
|
||
data: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
quantity: args.quantity,
|
||
},
|
||
})
|
||
}
|
||
|
||
// Возвращаем обновленную корзину
|
||
const updatedCart = await prisma.cart.findUnique({
|
||
where: { id: cart.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар добавлен в корзину',
|
||
cart: updatedCart,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding to cart:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при добавлении в корзину',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить количество товара в корзине
|
||
updateCartItem: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
const cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
})
|
||
|
||
if (!cart) {
|
||
return {
|
||
success: false,
|
||
message: 'Корзина не найдена',
|
||
}
|
||
}
|
||
|
||
// Проверяем, что товар существует в корзине
|
||
const cartItem = await prisma.cartItem.findUnique({
|
||
where: {
|
||
cartId_productId: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
include: {
|
||
product: true,
|
||
},
|
||
})
|
||
|
||
if (!cartItem) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден в корзине',
|
||
}
|
||
}
|
||
|
||
if (args.quantity <= 0) {
|
||
return {
|
||
success: false,
|
||
message: 'Количество должно быть больше 0',
|
||
}
|
||
}
|
||
|
||
if (args.quantity > cartItem.product.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}`,
|
||
}
|
||
}
|
||
|
||
try {
|
||
await prisma.cartItem.update({
|
||
where: { id: cartItem.id },
|
||
data: { quantity: args.quantity },
|
||
})
|
||
|
||
// Возвращаем обновленную корзину
|
||
const updatedCart = await prisma.cart.findUnique({
|
||
where: { id: cart.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Количество товара обновлено',
|
||
cart: updatedCart,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating cart item:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении корзины',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить товар из корзины
|
||
removeFromCart: async (_: unknown, args: { productId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
const cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
})
|
||
|
||
if (!cart) {
|
||
return {
|
||
success: false,
|
||
message: 'Корзина не найдена',
|
||
}
|
||
}
|
||
|
||
try {
|
||
await prisma.cartItem.delete({
|
||
where: {
|
||
cartId_productId: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
})
|
||
|
||
// Возвращаем обновленную корзину
|
||
const updatedCart = await prisma.cart.findUnique({
|
||
where: { id: cart.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар удален из корзины',
|
||
cart: updatedCart,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error removing from cart:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при удалении из корзины',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Очистить корзину
|
||
clearCart: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
const cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
})
|
||
|
||
if (!cart) {
|
||
return false
|
||
}
|
||
|
||
try {
|
||
await prisma.cartItem.deleteMany({
|
||
where: { cartId: cart.id },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error clearing cart:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Добавить товар в избранное
|
||
addToFavorites: async (_: unknown, args: { productId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что товар существует и активен
|
||
const product = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.productId,
|
||
isActive: true,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар не найден или неактивен',
|
||
}
|
||
}
|
||
|
||
// Проверяем, что пользователь не пытается добавить свой собственный товар
|
||
if (product.organizationId === currentUser.organization.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Нельзя добавлять собственные товары в избранное',
|
||
}
|
||
}
|
||
|
||
try {
|
||
// Проверяем, есть ли уже такой товар в избранном
|
||
const existingFavorite = await prisma.favorites.findUnique({
|
||
where: {
|
||
organizationId_productId: {
|
||
organizationId: currentUser.organization.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
})
|
||
|
||
if (existingFavorite) {
|
||
return {
|
||
success: false,
|
||
message: 'Товар уже в избранном',
|
||
}
|
||
}
|
||
|
||
// Добавляем товар в избранное
|
||
await prisma.favorites.create({
|
||
data: {
|
||
organizationId: currentUser.organization.id,
|
||
productId: args.productId,
|
||
},
|
||
})
|
||
|
||
// Возвращаем обновленный список избранного
|
||
const favorites = await prisma.favorites.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар добавлен в избранное',
|
||
favorites: favorites.map((favorite) => favorite.product),
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding to favorites:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при добавлении в избранное',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить товар из избранного
|
||
removeFromFavorites: async (_: unknown, args: { productId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Удаляем товар из избранного
|
||
await prisma.favorites.deleteMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
productId: args.productId,
|
||
},
|
||
})
|
||
|
||
// Возвращаем обновленный список избранного
|
||
const favorites = await prisma.favorites.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Товар удален из избранного',
|
||
favorites: favorites.map((favorite) => favorite.product),
|
||
}
|
||
} catch (error) {
|
||
console.error('Error removing from favorites:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при удалении из избранного',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Создать сотрудника
|
||
createEmployee: async (_: unknown, args: { input: CreateEmployeeInput }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
const employee = await prisma.employee.create({
|
||
data: {
|
||
...args.input,
|
||
organizationId: currentUser.organization.id,
|
||
birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined,
|
||
passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined,
|
||
hireDate: new Date(args.input.hireDate),
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Сотрудник успешно добавлен',
|
||
employee,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating employee:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании сотрудника',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить сотрудника
|
||
updateEmployee: async (_: unknown, args: { id: string; input: UpdateEmployeeInput }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
const employee = await prisma.employee.update({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
data: {
|
||
...args.input,
|
||
birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined,
|
||
passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined,
|
||
hireDate: args.input.hireDate ? new Date(args.input.hireDate) : undefined,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Сотрудник успешно обновлен',
|
||
employee,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating employee:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении сотрудника',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить сотрудника
|
||
deleteEmployee: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
await prisma.employee.delete({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error deleting employee:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Обновить табель сотрудника
|
||
updateEmployeeSchedule: async (_: unknown, args: { input: UpdateScheduleInput }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступно только для фулфилмент центров')
|
||
}
|
||
|
||
try {
|
||
// Проверяем что сотрудник принадлежит организации
|
||
const employee = await prisma.employee.findFirst({
|
||
where: {
|
||
id: args.input.employeeId,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!employee) {
|
||
throw new GraphQLError('Сотрудник не найден')
|
||
}
|
||
|
||
// Создаем или обновляем запись табеля
|
||
await prisma.employeeSchedule.upsert({
|
||
where: {
|
||
employeeId_date: {
|
||
employeeId: args.input.employeeId,
|
||
date: new Date(args.input.date),
|
||
},
|
||
},
|
||
create: {
|
||
employeeId: args.input.employeeId,
|
||
date: new Date(args.input.date),
|
||
status: args.input.status,
|
||
hoursWorked: args.input.hoursWorked,
|
||
overtimeHours: args.input.overtimeHours,
|
||
notes: args.input.notes,
|
||
},
|
||
update: {
|
||
status: args.input.status,
|
||
hoursWorked: args.input.hoursWorked,
|
||
overtimeHours: args.input.overtimeHours,
|
||
notes: args.input.notes,
|
||
},
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Error updating employee schedule:', error)
|
||
return false
|
||
}
|
||
},
|
||
|
||
// Создать поставку Wildberries
|
||
createWildberriesSupply: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
cards: Array<{
|
||
price: number
|
||
discountedPrice?: number
|
||
selectedQuantity: number
|
||
selectedServices?: string[]
|
||
}>
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Пока что просто логируем данные, так как таблицы еще нет
|
||
console.warn('Создание поставки Wildberries с данными:', args.input)
|
||
|
||
const totalAmount = args.input.cards.reduce((sum: number, card) => {
|
||
const cardPrice = card.discountedPrice || card.price
|
||
const servicesPrice = (card.selectedServices?.length || 0) * 50
|
||
return sum + (cardPrice + servicesPrice) * card.selectedQuantity
|
||
}, 0)
|
||
|
||
const totalItems = args.input.cards.reduce((sum: number, card) => sum + card.selectedQuantity, 0)
|
||
|
||
// Временная заглушка - вернем success без создания в БД
|
||
return {
|
||
success: true,
|
||
message: `Поставка создана успешно! Товаров: ${totalItems}, Сумма: ${totalAmount} руб.`,
|
||
supply: null, // Временно null
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating Wildberries supply:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании поставки Wildberries',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Создать поставщика для поставки
|
||
createSupplySupplier: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
name: string
|
||
contactName: string
|
||
phone: string
|
||
market?: string
|
||
address?: string
|
||
place?: string
|
||
telegram?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Создаем поставщика в базе данных
|
||
const supplier = await prisma.supplySupplier.create({
|
||
data: {
|
||
name: args.input.name,
|
||
contactName: args.input.contactName,
|
||
phone: args.input.phone,
|
||
market: args.input.market,
|
||
address: args.input.address,
|
||
place: args.input.place,
|
||
telegram: args.input.telegram,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Поставщик добавлен успешно!',
|
||
supplier: {
|
||
id: supplier.id,
|
||
name: supplier.name,
|
||
contactName: supplier.contactName,
|
||
phone: supplier.phone,
|
||
market: supplier.market,
|
||
address: supplier.address,
|
||
place: supplier.place,
|
||
telegram: supplier.telegram,
|
||
createdAt: supplier.createdAt,
|
||
},
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating supply supplier:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при добавлении поставщика',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить статус заказа поставки
|
||
updateSupplyOrderStatus: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
status:
|
||
| 'PENDING'
|
||
| 'CONFIRMED'
|
||
| 'IN_TRANSIT'
|
||
| 'SUPPLIER_APPROVED'
|
||
| 'LOGISTICS_CONFIRMED'
|
||
| 'SHIPPED'
|
||
| 'DELIVERED'
|
||
| 'CANCELLED'
|
||
},
|
||
context: Context,
|
||
) => {
|
||
console.warn(`[DEBUG] updateSupplyOrderStatus вызван для заказа ${args.id} со статусом ${args.status}`)
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Находим заказ поставки
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
OR: [
|
||
{ organizationId: currentUser.organization.id }, // Создатель заказа
|
||
{ partnerId: currentUser.organization.id }, // Поставщик
|
||
{ fulfillmentCenterId: currentUser.organization.id }, // Фулфилмент-центр
|
||
],
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
partner: true,
|
||
fulfillmentCenter: true,
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
throw new GraphQLError('Заказ поставки не найден или нет доступа')
|
||
}
|
||
|
||
// Обновляем статус заказа
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: args.status },
|
||
include: {
|
||
partner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// ОТКЛЮЧЕНО: Устаревшая логика для обновления расходников
|
||
// Теперь используются специальные мутации для каждой роли
|
||
const targetOrganizationId = existingOrder.fulfillmentCenterId || existingOrder.organizationId
|
||
|
||
if (args.status === 'CONFIRMED') {
|
||
console.warn(`[WARNING] Попытка использовать устаревший статус CONFIRMED для заказа ${args.id}`)
|
||
// Не обновляем расходники для устаревших статусов
|
||
// await prisma.supply.updateMany({
|
||
// where: {
|
||
// organizationId: targetOrganizationId,
|
||
// status: "planned",
|
||
// name: {
|
||
// in: existingOrder.items.map(item => item.product.name)
|
||
// }
|
||
// },
|
||
// data: {
|
||
// status: "confirmed"
|
||
// }
|
||
// });
|
||
|
||
console.warn("✅ Статусы расходников обновлены на 'confirmed'")
|
||
}
|
||
|
||
if (args.status === 'IN_TRANSIT') {
|
||
// При отгрузке - переводим расходники в статус "in-transit"
|
||
await prisma.supply.updateMany({
|
||
where: {
|
||
organizationId: targetOrganizationId,
|
||
status: 'confirmed',
|
||
name: {
|
||
in: existingOrder.items.map((item) => item.product.name),
|
||
},
|
||
},
|
||
data: {
|
||
status: 'in-transit',
|
||
},
|
||
})
|
||
|
||
console.warn("✅ Статусы расходников обновлены на 'in-transit'")
|
||
}
|
||
|
||
// Если статус изменился на DELIVERED, обновляем склад
|
||
if (args.status === 'DELIVERED') {
|
||
console.warn('🚚 Обновляем склад организации:', {
|
||
targetOrganizationId,
|
||
fulfillmentCenterId: existingOrder.fulfillmentCenterId,
|
||
organizationId: existingOrder.organizationId,
|
||
itemsCount: existingOrder.items.length,
|
||
items: existingOrder.items.map((item) => ({
|
||
productName: item.product.name,
|
||
quantity: item.quantity,
|
||
})),
|
||
})
|
||
|
||
// 🔄 СИНХРОНИЗАЦИЯ: Обновляем товары поставщика (переводим из "в пути" в "продано" + обновляем основные остатки)
|
||
for (const item of existingOrder.items) {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: item.product.id },
|
||
})
|
||
|
||
if (product) {
|
||
// ИСПРАВЛЕНО: НЕ списываем повторно, только переводим из inTransit в sold
|
||
// Остаток уже был уменьшен при создании/одобрении заказа
|
||
await prisma.product.update({
|
||
where: { id: item.product.id },
|
||
data: {
|
||
// НЕ ТРОГАЕМ stock - он уже правильно уменьшен при заказе
|
||
// Только переводим из inTransit в sold
|
||
inTransit: Math.max((product.inTransit || 0) - item.quantity, 0),
|
||
sold: (product.sold || 0) + item.quantity,
|
||
},
|
||
})
|
||
console.warn(
|
||
`✅ Товар поставщика "${product.name}" обновлен: доставлено ${
|
||
item.quantity
|
||
} единиц (остаток НЕ ИЗМЕНЕН: ${product.stock || product.quantity || 0})`,
|
||
)
|
||
}
|
||
}
|
||
|
||
// Обновляем расходники
|
||
for (const item of existingOrder.items) {
|
||
console.warn('📦 Обрабатываем товар:', {
|
||
productName: item.product.name,
|
||
quantity: item.quantity,
|
||
targetOrganizationId,
|
||
consumableType: existingOrder.consumableType,
|
||
})
|
||
|
||
// ИСПРАВЛЕНИЕ: Определяем правильный тип расходников
|
||
const isSellerSupply = existingOrder.consumableType === 'SELLER_CONSUMABLES'
|
||
const supplyType = isSellerSupply ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES'
|
||
const sellerOwnerId = isSellerSupply ? existingOrder.organizationId : null
|
||
|
||
console.warn('🔍 Определен тип расходников:', {
|
||
isSellerSupply,
|
||
supplyType,
|
||
sellerOwnerId,
|
||
})
|
||
|
||
// ИСПРАВЛЕНИЕ: Ищем по Артикул СФ для уникальности вместо имени
|
||
const whereCondition = isSellerSupply
|
||
? {
|
||
organizationId: targetOrganizationId,
|
||
article: item.product.article, // ИЗМЕНЕНО: поиск по article вместо name
|
||
type: 'SELLER_CONSUMABLES' as const,
|
||
sellerOwnerId: existingOrder.organizationId,
|
||
}
|
||
: {
|
||
organizationId: targetOrganizationId,
|
||
article: item.product.article, // ИЗМЕНЕНО: поиск по article вместо name
|
||
type: 'FULFILLMENT_CONSUMABLES' as const,
|
||
sellerOwnerId: null, // Для фулфилмента sellerOwnerId должен быть null
|
||
}
|
||
|
||
console.warn('🔍 Ищем существующий расходник с условиями:', whereCondition)
|
||
|
||
const existingSupply = await prisma.supply.findFirst({
|
||
where: whereCondition,
|
||
})
|
||
|
||
if (existingSupply) {
|
||
console.warn('📈 ОБНОВЛЯЕМ существующий расходник:', {
|
||
id: existingSupply.id,
|
||
oldStock: existingSupply.currentStock,
|
||
oldQuantity: existingSupply.quantity,
|
||
addingQuantity: item.quantity,
|
||
})
|
||
|
||
// ОБНОВЛЯЕМ существующий расходник
|
||
const updatedSupply = await prisma.supply.update({
|
||
where: { id: existingSupply.id },
|
||
data: {
|
||
currentStock: existingSupply.currentStock + item.quantity,
|
||
// ❌ ИСПРАВЛЕНО: НЕ обновляем quantity - это изначальное количество заказа!
|
||
// quantity остается как было изначально заказано
|
||
status: 'in-stock', // Меняем статус на "на складе"
|
||
updatedAt: new Date(),
|
||
},
|
||
})
|
||
|
||
console.warn('✅ Расходник ОБНОВЛЕН (НЕ создан дубликат):', {
|
||
id: updatedSupply.id,
|
||
name: updatedSupply.name,
|
||
newCurrentStock: updatedSupply.currentStock,
|
||
newTotalQuantity: updatedSupply.quantity,
|
||
type: updatedSupply.type,
|
||
})
|
||
} else {
|
||
console.warn('➕ СОЗДАЕМ новый расходник (не найден существующий):', {
|
||
name: item.product.name,
|
||
quantity: item.quantity,
|
||
organizationId: targetOrganizationId,
|
||
type: supplyType,
|
||
sellerOwnerId: sellerOwnerId,
|
||
})
|
||
|
||
// СОЗДАЕМ новый расходник
|
||
const newSupply = await prisma.supply.create({
|
||
data: {
|
||
name: item.product.name,
|
||
article: item.product.article, // ДОБАВЛЕНО: Артикул СФ для уникальности
|
||
description: item.product.description || `Поставка от ${existingOrder.partner.name}`,
|
||
price: item.price, // Цена закупки у поставщика
|
||
quantity: item.quantity,
|
||
unit: 'шт',
|
||
category: item.product.category?.name || 'Расходники',
|
||
status: 'in-stock',
|
||
date: new Date(),
|
||
supplier: existingOrder.partner.name || existingOrder.partner.fullName || 'Не указан',
|
||
minStock: Math.round(item.quantity * 0.1),
|
||
currentStock: item.quantity,
|
||
organizationId: targetOrganizationId,
|
||
type: supplyType as 'SELLER_CONSUMABLES' | 'FULFILLMENT_CONSUMABLES',
|
||
sellerOwnerId: sellerOwnerId,
|
||
},
|
||
})
|
||
|
||
console.warn('✅ Новый расходник СОЗДАН:', {
|
||
id: newSupply.id,
|
||
name: newSupply.name,
|
||
currentStock: newSupply.currentStock,
|
||
type: newSupply.type,
|
||
sellerOwnerId: newSupply.sellerOwnerId,
|
||
})
|
||
}
|
||
}
|
||
|
||
console.warn('🎉 Склад организации успешно обновлен!')
|
||
}
|
||
|
||
// Уведомляем вовлеченные организации об изменении статуса заказа
|
||
try {
|
||
const orgIds = [
|
||
existingOrder.organizationId,
|
||
existingOrder.partnerId,
|
||
existingOrder.fulfillmentCenterId || undefined,
|
||
].filter(Boolean) as string[]
|
||
notifyMany(orgIds, {
|
||
type: 'supply-order:updated',
|
||
payload: { id: updatedOrder.id, status: updatedOrder.status },
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: `Статус заказа поставки обновлен на "${args.status}"`,
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating supply order status:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении статуса заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновление параметров поставки (объём и грузовые места)
|
||
updateSupplyParameters: async (
|
||
_: unknown,
|
||
args: { id: string; volume?: number; packagesCount?: number },
|
||
context: GraphQLContext,
|
||
) => {
|
||
try {
|
||
// Проверка аутентификации
|
||
if (!context.user?.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Необходима аутентификация',
|
||
}
|
||
}
|
||
|
||
// Найти поставку и проверить права доступа
|
||
const supply = await prisma.supplyOrder.findUnique({
|
||
where: { id: args.id },
|
||
include: { partner: true },
|
||
})
|
||
|
||
if (!supply) {
|
||
return {
|
||
success: false,
|
||
message: 'Поставка не найдена',
|
||
}
|
||
}
|
||
|
||
// Проверить, что пользователь - поставщик этой заявки
|
||
if (supply.partnerId !== context.user.organization?.id) {
|
||
return {
|
||
success: false,
|
||
message: 'Недостаточно прав для изменения данной поставки',
|
||
}
|
||
}
|
||
|
||
// Подготовить данные для обновления
|
||
const updateData: { volume?: number; packagesCount?: number } = {}
|
||
if (args.volume !== undefined) updateData.volume = args.volume
|
||
if (args.packagesCount !== undefined) updateData.packagesCount = args.packagesCount
|
||
|
||
// Обновить поставку
|
||
const updatedSupply = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: updateData,
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Параметры поставки обновлены',
|
||
order: updatedSupply,
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при обновлении параметров поставки:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении параметров поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Назначение логистики фулфилментом на заказ селлера
|
||
assignLogisticsToSupply: async (
|
||
_: unknown,
|
||
args: {
|
||
supplyOrderId: string
|
||
logisticsPartnerId: string
|
||
responsibleId?: string
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что пользователь - фулфилмент
|
||
if (currentUser.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Только фулфилмент может назначать логистику')
|
||
}
|
||
|
||
try {
|
||
// Находим заказ
|
||
const existingOrder = await prisma.supplyOrder.findUnique({
|
||
where: { id: args.supplyOrderId },
|
||
include: {
|
||
partner: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
throw new GraphQLError('Заказ поставки не найден')
|
||
}
|
||
|
||
// Проверяем, что это заказ для нашего фулфилмент-центра
|
||
if (existingOrder.fulfillmentCenterId !== currentUser.organization.id) {
|
||
throw new GraphQLError('Нет доступа к этому заказу')
|
||
}
|
||
|
||
// Проверяем, что статус позволяет назначить логистику
|
||
if (existingOrder.status !== 'SUPPLIER_APPROVED') {
|
||
throw new GraphQLError(`Нельзя назначить логистику для заказа со статусом ${existingOrder.status}`)
|
||
}
|
||
|
||
// Проверяем, что логистическая компания существует
|
||
const logisticsPartner = await prisma.organization.findUnique({
|
||
where: { id: args.logisticsPartnerId },
|
||
})
|
||
|
||
if (!logisticsPartner || logisticsPartner.type !== 'LOGIST') {
|
||
throw new GraphQLError('Логистическая компания не найдена')
|
||
}
|
||
|
||
// Обновляем заказ
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.supplyOrderId },
|
||
data: {
|
||
logisticsPartner: {
|
||
connect: { id: args.logisticsPartnerId },
|
||
},
|
||
status: 'CONFIRMED', // Переводим в статус "подтвержден фулфилментом"
|
||
},
|
||
include: {
|
||
partner: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: { product: true },
|
||
},
|
||
},
|
||
})
|
||
|
||
console.warn(`✅ Логистика назначена на заказ ${args.supplyOrderId}:`, {
|
||
logisticsPartner: logisticsPartner.name,
|
||
responsible: args.responsibleId,
|
||
newStatus: 'CONFIRMED',
|
||
})
|
||
|
||
try {
|
||
const orgIds = [
|
||
existingOrder.organizationId,
|
||
existingOrder.partnerId,
|
||
existingOrder.fulfillmentCenterId || undefined,
|
||
args.logisticsPartnerId,
|
||
].filter(Boolean) as string[]
|
||
notifyMany(orgIds, {
|
||
type: 'supply-order:updated',
|
||
payload: { id: updatedOrder.id, status: updatedOrder.status },
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Логистика успешно назначена',
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Ошибка при назначении логистики:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка при назначении логистики',
|
||
}
|
||
}
|
||
},
|
||
|
||
// 🔒 МУТАЦИИ ПОСТАВЩИКА С СИСТЕМОЙ БЕЗОПАСНОСТИ
|
||
supplierApproveOrder: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА
|
||
if (currentUser.organization.type !== 'WHOLESALE') {
|
||
throw new GraphQLError('Доступ разрешен только поставщикам (WHOLESALE)')
|
||
}
|
||
|
||
try {
|
||
// 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ
|
||
const securityContext: SecurityContext = {
|
||
userId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
userRole: currentUser.organization.type,
|
||
requestMetadata: {
|
||
action: 'APPROVE_ORDER',
|
||
resourceId: args.id,
|
||
timestamp: new Date().toISOString(),
|
||
ipAddress: context.req?.ip || 'unknown',
|
||
userAgent: context.req?.get('user-agent') || 'unknown',
|
||
},
|
||
}
|
||
|
||
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
|
||
await ParticipantIsolation.validateAccess(
|
||
prisma,
|
||
currentUser.organization.id,
|
||
currentUser.organization.type,
|
||
'SUPPLY_ORDER',
|
||
)
|
||
// 🔒 ПОЛУЧЕНИЕ ЗАКАЗА С ПРОВЕРКОЙ ДОСТУПА
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
partnerId: currentUser.organization.id, // Только поставщик может одобрить
|
||
status: 'PENDING', // Можно одобрить только заказы в статусе PENDING
|
||
},
|
||
include: {
|
||
organization: true,
|
||
partner: true,
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для одобрения',
|
||
}
|
||
}
|
||
|
||
// 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ
|
||
await ParticipantIsolation.validatePartnerAccess(
|
||
prisma,
|
||
currentUser.organization.id,
|
||
existingOrder.organizationId,
|
||
)
|
||
|
||
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
|
||
await CommercialDataAudit.logAccess(prisma, {
|
||
userId: currentUser.id,
|
||
organizationType: currentUser.organization.type,
|
||
action: 'APPROVE_ORDER',
|
||
resourceType: 'SUPPLY_ORDER',
|
||
resourceId: args.id,
|
||
metadata: {
|
||
partnerOrganizationId: existingOrder.organizationId,
|
||
orderValue: existingOrder.totalAmount?.toString() || '0',
|
||
...securityContext.requestMetadata,
|
||
},
|
||
})
|
||
|
||
console.warn(`[DEBUG] Поставщик ${currentUser.organization.name} одобряет заказ ${args.id}`)
|
||
|
||
// 🔄 СИНХРОНИЗАЦИЯ ОСТАТКОВ: Резервируем товары у поставщика
|
||
const orderWithItems = await prisma.supplyOrder.findUnique({
|
||
where: { id: args.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (orderWithItems) {
|
||
for (const item of orderWithItems.items) {
|
||
// Резервируем товар (увеличиваем поле ordered)
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: item.product.id },
|
||
})
|
||
|
||
if (product) {
|
||
const availableStock = (product.stock || product.quantity) - (product.ordered || 0)
|
||
|
||
if (availableStock < item.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара "${product.name}" на складе. Доступно: ${availableStock}, требуется: ${item.quantity}`,
|
||
}
|
||
}
|
||
|
||
// Согласно правилам: при одобрении заказа остаток должен уменьшиться
|
||
const currentStock = product.stock || product.quantity || 0
|
||
const newStock = Math.max(currentStock - item.quantity, 0)
|
||
|
||
await prisma.product.update({
|
||
where: { id: item.product.id },
|
||
data: {
|
||
// Уменьшаем основной остаток (товар зарезервирован для заказа)
|
||
stock: newStock,
|
||
quantity: newStock, // Синхронизируем оба поля для совместимости
|
||
// Увеличиваем количество заказанного (для отслеживания)
|
||
ordered: (product.ordered || 0) + item.quantity,
|
||
},
|
||
})
|
||
|
||
console.warn(`📦 Товар "${product.name}" зарезервирован: ${item.quantity} единиц`)
|
||
console.warn(` 📊 Остаток: ${currentStock} -> ${newStock} (уменьшен на ${item.quantity})`)
|
||
console.warn(` 📋 Заказано: ${product.ordered || 0} -> ${(product.ordered || 0) + item.quantity}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'SUPPLIER_APPROVED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
console.warn('[DEBUG] updatedOrder structure:', {
|
||
id: updatedOrder.id,
|
||
itemsCount: updatedOrder.items?.length || 0,
|
||
firstItem: updatedOrder.items?.[0] ? {
|
||
productId: updatedOrder.items[0].productId,
|
||
hasProduct: !!updatedOrder.items[0].product,
|
||
productOrgId: updatedOrder.items[0].product?.organizationId,
|
||
hasProductOrg: !!updatedOrder.items[0].product?.organization,
|
||
} : null,
|
||
currentUserOrgId: currentUser.organization.id,
|
||
})
|
||
|
||
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
|
||
const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser)
|
||
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType)
|
||
|
||
console.warn(`[DEBUG] Заказ ${args.id} успешно обновлен до статуса: ${updatedOrder.status}`)
|
||
console.warn('[DEBUG] filteredOrder:', {
|
||
hasData: !!filteredOrder.data,
|
||
dataId: filteredOrder.data?.id,
|
||
dataKeys: Object.keys(filteredOrder.data || {}),
|
||
})
|
||
|
||
try {
|
||
const orgIds = [
|
||
updatedOrder.organizationId,
|
||
updatedOrder.partnerId,
|
||
updatedOrder.fulfillmentCenterId || undefined,
|
||
updatedOrder.logisticsPartnerId || undefined,
|
||
].filter(Boolean) as string[]
|
||
notifyMany(orgIds, {
|
||
type: 'supply-order:updated',
|
||
payload: { id: updatedOrder.id, status: updatedOrder.status },
|
||
})
|
||
} catch {}
|
||
|
||
// Проверка на случай, если фильтрованные данные null
|
||
if (!filteredOrder.data || !filteredOrder.data.id) {
|
||
console.error('[ERROR] filteredOrder.data is null or missing id:', filteredOrder)
|
||
throw new GraphQLError('Filtered order data is invalid')
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Заказ поставки одобрен поставщиком. Товары зарезервированы, остатки обновлены.',
|
||
order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error approving supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при одобрении заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
supplierRejectOrder: async (_: unknown, args: { id: string; reason?: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА
|
||
if (currentUser.organization.type !== 'WHOLESALE') {
|
||
throw new GraphQLError('Доступ разрешен только поставщикам (WHOLESALE)')
|
||
}
|
||
|
||
try {
|
||
// 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ
|
||
const securityContext: SecurityContext = {
|
||
userId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
userRole: currentUser.organization.type,
|
||
requestMetadata: {
|
||
action: 'REJECT_ORDER',
|
||
resourceId: args.id,
|
||
timestamp: new Date().toISOString(),
|
||
ipAddress: context.req?.ip || 'unknown',
|
||
userAgent: context.req?.get('user-agent') || 'unknown',
|
||
},
|
||
}
|
||
|
||
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
|
||
await ParticipantIsolation.validateAccess(
|
||
prisma,
|
||
currentUser.organization.id,
|
||
currentUser.organization.type,
|
||
'SUPPLY_ORDER',
|
||
)
|
||
|
||
// 🔒 ПОЛУЧЕНИЕ ЗАКАЗА С ПРОВЕРКОЙ ДОСТУПА
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
partnerId: currentUser.organization.id,
|
||
status: 'PENDING',
|
||
},
|
||
include: {
|
||
organization: true,
|
||
partner: true,
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для отклонения',
|
||
}
|
||
}
|
||
|
||
// 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ
|
||
await ParticipantIsolation.validatePartnerAccess(
|
||
prisma,
|
||
currentUser.organization.id,
|
||
existingOrder.organizationId,
|
||
)
|
||
|
||
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
|
||
await CommercialDataAudit.logAccess(prisma, {
|
||
userId: currentUser.id,
|
||
organizationType: currentUser.organization.type,
|
||
action: 'REJECT_ORDER',
|
||
resourceType: 'SUPPLY_ORDER',
|
||
resourceId: args.id,
|
||
metadata: {
|
||
partnerOrganizationId: existingOrder.organizationId,
|
||
orderValue: existingOrder.totalAmount?.toString() || '0',
|
||
rejectionReason: args.reason,
|
||
...securityContext.requestMetadata,
|
||
},
|
||
})
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'CANCELLED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
|
||
const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser)
|
||
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType)
|
||
|
||
// 📦 СНИМАЕМ РЕЗЕРВАЦИЮ ПРИ ОТКЛОНЕНИИ
|
||
// Восстанавливаем остатки и убираем резервацию для каждого отклоненного товара
|
||
for (const item of updatedOrder.items) {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: item.productId },
|
||
})
|
||
|
||
if (product) {
|
||
// Восстанавливаем основные остатки (на случай, если заказ был одобрен, а затем отклонен)
|
||
const currentStock = product.stock || product.quantity || 0
|
||
const restoredStock = currentStock + item.quantity
|
||
|
||
await prisma.product.update({
|
||
where: { id: item.productId },
|
||
data: {
|
||
// Восстанавливаем основной остаток
|
||
stock: restoredStock,
|
||
quantity: restoredStock,
|
||
// Уменьшаем количество заказанного
|
||
ordered: Math.max((product.ordered || 0) - item.quantity, 0),
|
||
},
|
||
})
|
||
|
||
console.warn(
|
||
`🔄 Восстановлены остатки товара "${product.name}": ${currentStock} -> ${restoredStock}, ordered: ${
|
||
product.ordered
|
||
} -> ${Math.max((product.ordered || 0) - item.quantity, 0)}`,
|
||
)
|
||
}
|
||
}
|
||
|
||
console.warn(
|
||
`📦 Снята резервация при отклонении заказа ${updatedOrder.id}:`,
|
||
updatedOrder.items.map((item) => `${item.productId}: -${item.quantity} шт.`).join(', '),
|
||
)
|
||
|
||
try {
|
||
const orgIds = [
|
||
updatedOrder.organizationId,
|
||
updatedOrder.partnerId,
|
||
updatedOrder.fulfillmentCenterId || undefined,
|
||
updatedOrder.logisticsPartnerId || undefined,
|
||
].filter(Boolean) as string[]
|
||
notifyMany(orgIds, {
|
||
type: 'supply-order:updated',
|
||
payload: { id: updatedOrder.id, status: updatedOrder.status },
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: args.reason ? `Заказ отклонен поставщиком. Причина: ${args.reason}` : 'Заказ отклонен поставщиком',
|
||
order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error rejecting supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отклонении заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
supplierShipOrder: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА
|
||
if (currentUser.organization.type !== 'WHOLESALE') {
|
||
throw new GraphQLError('Доступ разрешен только поставщикам (WHOLESALE)')
|
||
}
|
||
|
||
try {
|
||
// 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ
|
||
const securityContext: SecurityContext = {
|
||
userId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
userRole: currentUser.organization.type,
|
||
requestMetadata: {
|
||
action: 'SHIP_ORDER',
|
||
resourceId: args.id,
|
||
timestamp: new Date().toISOString(),
|
||
ipAddress: context.req?.ip || 'unknown',
|
||
userAgent: context.req?.get('user-agent') || 'unknown',
|
||
},
|
||
}
|
||
|
||
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
|
||
await ParticipantIsolation.validateAccess(
|
||
prisma,
|
||
currentUser.organization.id,
|
||
currentUser.organization.type,
|
||
'SUPPLY_ORDER',
|
||
)
|
||
|
||
// 🔒 ПОЛУЧЕНИЕ ЗАКАЗА С ПРОВЕРКОЙ ДОСТУПА
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
partnerId: currentUser.organization.id,
|
||
status: 'LOGISTICS_CONFIRMED',
|
||
},
|
||
include: {
|
||
organization: true,
|
||
partner: true,
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для отправки',
|
||
}
|
||
}
|
||
|
||
// 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ
|
||
await ParticipantIsolation.validatePartnerAccess(
|
||
prisma,
|
||
currentUser.organization.id,
|
||
existingOrder.organizationId,
|
||
)
|
||
|
||
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
|
||
await CommercialDataAudit.logAccess(prisma, {
|
||
userId: currentUser.id,
|
||
organizationType: currentUser.organization.type,
|
||
action: 'SHIP_ORDER',
|
||
resourceType: 'SUPPLY_ORDER',
|
||
resourceId: args.id,
|
||
metadata: {
|
||
partnerOrganizationId: existingOrder.organizationId,
|
||
orderValue: existingOrder.totalAmount?.toString() || '0',
|
||
...securityContext.requestMetadata,
|
||
},
|
||
})
|
||
|
||
// 🔄 СИНХРОНИЗАЦИЯ ОСТАТКОВ: Переводим товары из "заказано" в "в пути"
|
||
const orderWithItems = await prisma.supplyOrder.findUnique({
|
||
where: { id: args.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (orderWithItems) {
|
||
for (const item of orderWithItems.items) {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: item.product.id },
|
||
})
|
||
|
||
if (product) {
|
||
await prisma.product.update({
|
||
where: { id: item.product.id },
|
||
data: {
|
||
ordered: Math.max((product.ordered || 0) - item.quantity, 0),
|
||
inTransit: (product.inTransit || 0) + item.quantity,
|
||
},
|
||
})
|
||
|
||
console.warn(`🚚 Товар "${product.name}" переведен в статус "в пути": ${item.quantity} единиц`)
|
||
}
|
||
}
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'SHIPPED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
recipe: {
|
||
include: {
|
||
services: true,
|
||
fulfillmentConsumables: true,
|
||
sellerConsumables: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
|
||
const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser)
|
||
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType)
|
||
|
||
try {
|
||
const orgIds = [
|
||
updatedOrder.organizationId,
|
||
updatedOrder.partnerId,
|
||
updatedOrder.fulfillmentCenterId || undefined,
|
||
updatedOrder.logisticsPartnerId || undefined,
|
||
].filter(Boolean) as string[]
|
||
notifyMany(orgIds, {
|
||
type: 'supply-order:updated',
|
||
payload: { id: updatedOrder.id, status: updatedOrder.status },
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: "Заказ отправлен поставщиком. Товары переведены в статус 'в пути'.",
|
||
order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error shipping supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отправке заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
logisticsConfirmOrder: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
logisticsPartnerId: currentUser.organization.id,
|
||
OR: [{ status: 'SUPPLIER_APPROVED' }, { status: 'CONFIRMED' }],
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для подтверждения логистикой',
|
||
}
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'LOGISTICS_CONFIRMED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
try {
|
||
const orgIds = [
|
||
updatedOrder.organizationId,
|
||
updatedOrder.partnerId,
|
||
updatedOrder.fulfillmentCenterId || undefined,
|
||
updatedOrder.logisticsPartnerId || undefined,
|
||
].filter(Boolean) as string[]
|
||
notifyMany(orgIds, {
|
||
type: 'supply-order:updated',
|
||
payload: { id: updatedOrder.id, status: updatedOrder.status },
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Заказ подтвержден логистической компанией',
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error confirming supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при подтверждении заказа логистикой',
|
||
}
|
||
}
|
||
},
|
||
|
||
logisticsRejectOrder: async (_: unknown, args: { id: string; reason?: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
logisticsPartnerId: currentUser.organization.id,
|
||
OR: [{ status: 'SUPPLIER_APPROVED' }, { status: 'CONFIRMED' }],
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для отклонения логистикой',
|
||
}
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'CANCELLED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
try {
|
||
const orgIds = [
|
||
updatedOrder.organizationId,
|
||
updatedOrder.partnerId,
|
||
updatedOrder.fulfillmentCenterId || undefined,
|
||
updatedOrder.logisticsPartnerId || undefined,
|
||
].filter(Boolean) as string[]
|
||
notifyMany(orgIds, {
|
||
type: 'supply-order:updated',
|
||
payload: { id: updatedOrder.id, status: updatedOrder.status },
|
||
})
|
||
} catch {}
|
||
|
||
return {
|
||
success: true,
|
||
message: args.reason
|
||
? `Заказ отклонен логистической компанией. Причина: ${args.reason}`
|
||
: 'Заказ отклонен логистической компанией',
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error rejecting supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при отклонении заказа логистикой',
|
||
}
|
||
}
|
||
},
|
||
|
||
fulfillmentReceiveOrder: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
fulfillmentCenterId: currentUser.organization.id,
|
||
status: 'SHIPPED',
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true, // Селлер-создатель заказа
|
||
partner: true, // Поставщик
|
||
},
|
||
})
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ не найден или недоступен для приема',
|
||
}
|
||
}
|
||
|
||
// Обновляем статус заказа
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: 'DELIVERED' },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// 🔄 СИНХРОНИЗАЦИЯ СКЛАДА ПОСТАВЩИКА: Обновляем остатки поставщика согласно правилам
|
||
console.warn('🔄 Начинаем синхронизацию остатков поставщика...')
|
||
for (const item of existingOrder.items) {
|
||
const product = await prisma.product.findUnique({
|
||
where: { id: item.product.id },
|
||
})
|
||
|
||
if (product) {
|
||
// ИСПРАВЛЕНО: НЕ списываем повторно, только переводим из inTransit в sold
|
||
// Остаток уже был уменьшен при создании/одобрении заказа
|
||
await prisma.product.update({
|
||
where: { id: item.product.id },
|
||
data: {
|
||
// НЕ ТРОГАЕМ stock - он уже правильно уменьшен при заказе
|
||
// Только переводим из inTransit в sold
|
||
inTransit: Math.max((product.inTransit || 0) - item.quantity, 0),
|
||
sold: (product.sold || 0) + item.quantity,
|
||
},
|
||
})
|
||
console.warn(`✅ Товар поставщика "${product.name}" обновлен: получено ${item.quantity} единиц`)
|
||
console.warn(
|
||
` 📊 Остаток: ${product.stock || product.quantity || 0} (НЕ ИЗМЕНЕН - уже списан при заказе)`,
|
||
)
|
||
console.warn(
|
||
` 🚚 В пути: ${product.inTransit || 0} -> ${Math.max(
|
||
(product.inTransit || 0) - item.quantity,
|
||
0,
|
||
)} (УБЫЛО: ${item.quantity})`,
|
||
)
|
||
console.warn(
|
||
` 💰 Продано: ${product.sold || 0} -> ${
|
||
(product.sold || 0) + item.quantity
|
||
} (ПРИБЫЛО: ${item.quantity})`,
|
||
)
|
||
}
|
||
}
|
||
|
||
// Обновляем склад фулфилмента с учетом типа расходников
|
||
console.warn('📦 Обновляем склад фулфилмента...')
|
||
console.warn(`🏷️ Тип поставки: ${existingOrder.consumableType || 'FULFILLMENT_CONSUMABLES'}`)
|
||
|
||
for (const item of existingOrder.items) {
|
||
// Определяем тип расходников и владельца
|
||
const isSellerSupply = existingOrder.consumableType === 'SELLER_CONSUMABLES'
|
||
const supplyType = isSellerSupply ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES'
|
||
const sellerOwnerId = isSellerSupply ? updatedOrder.organization?.id : null
|
||
|
||
// Для расходников селлеров ищем по Артикул СФ И по владельцу
|
||
const whereCondition = isSellerSupply
|
||
? {
|
||
organizationId: currentUser.organization.id,
|
||
article: item.product.article, // ИЗМЕНЕНО: поиск по article вместо name
|
||
type: 'SELLER_CONSUMABLES' as const,
|
||
sellerOwnerId: sellerOwnerId,
|
||
}
|
||
: {
|
||
organizationId: currentUser.organization.id,
|
||
article: item.product.article, // ИЗМЕНЕНО: поиск по article вместо name
|
||
type: 'FULFILLMENT_CONSUMABLES' as const,
|
||
}
|
||
|
||
const existingSupply = await prisma.supply.findFirst({
|
||
where: whereCondition,
|
||
})
|
||
|
||
if (existingSupply) {
|
||
await prisma.supply.update({
|
||
where: { id: existingSupply.id },
|
||
data: {
|
||
currentStock: existingSupply.currentStock + item.quantity,
|
||
// ❌ ИСПРАВЛЕНО: НЕ обновляем quantity - это изначальное количество заказа!
|
||
status: 'in-stock',
|
||
},
|
||
})
|
||
console.warn(
|
||
`📈 Обновлен существующий ${
|
||
isSellerSupply ? 'расходник селлера' : 'расходник фулфилмента'
|
||
} "${item.product.name}" ${
|
||
isSellerSupply ? `(владелец: ${updatedOrder.organization?.name})` : ''
|
||
}: ${existingSupply.currentStock} -> ${existingSupply.currentStock + item.quantity}`,
|
||
)
|
||
} else {
|
||
await prisma.supply.create({
|
||
data: {
|
||
name: item.product.name,
|
||
article: item.product.article, // ДОБАВЛЕНО: Артикул СФ для уникальности
|
||
description: isSellerSupply
|
||
? `Расходники селлера ${updatedOrder.organization?.name || updatedOrder.organization?.fullName}`
|
||
: item.product.description || `Расходники от ${updatedOrder.partner.name}`,
|
||
price: item.price, // Цена закупки у поставщика
|
||
quantity: item.quantity,
|
||
actualQuantity: item.quantity, // НОВОЕ: Фактически поставленное количество
|
||
currentStock: item.quantity,
|
||
usedStock: 0,
|
||
unit: 'шт',
|
||
category: item.product.category?.name || 'Расходники',
|
||
status: 'in-stock',
|
||
supplier: updatedOrder.partner.name || updatedOrder.partner.fullName || 'Поставщик',
|
||
type: supplyType as 'SELLER_CONSUMABLES' | 'FULFILLMENT_CONSUMABLES',
|
||
sellerOwnerId: sellerOwnerId,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
console.warn(
|
||
`➕ Создан новый ${
|
||
isSellerSupply ? 'расходник селлера' : 'расходник фулфилмента'
|
||
} "${item.product.name}" ${
|
||
isSellerSupply ? `(владелец: ${updatedOrder.organization?.name})` : ''
|
||
}: ${item.quantity} единиц`,
|
||
)
|
||
}
|
||
}
|
||
|
||
console.warn('🎉 Синхронизация склада завершена успешно!')
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Заказ принят фулфилментом. Склад обновлен. Остатки поставщика синхронизированы.',
|
||
order: updatedOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error receiving supply order:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при приеме заказа поставки',
|
||
}
|
||
}
|
||
},
|
||
|
||
updateExternalAdClicks: async (_: unknown, { id, clicks }: { id: string; clicks: number }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Проверяем, что реклама принадлежит организации пользователя
|
||
const existingAd = await prisma.externalAd.findFirst({
|
||
where: {
|
||
id,
|
||
organizationId: user.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingAd) {
|
||
throw new GraphQLError('Внешняя реклама не найдена')
|
||
}
|
||
|
||
await prisma.externalAd.update({
|
||
where: { id },
|
||
data: { clicks },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Клики успешно обновлены',
|
||
externalAd: null,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating external ad clicks:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка обновления кликов',
|
||
externalAd: null,
|
||
}
|
||
}
|
||
},
|
||
},
|
||
|
||
// Резолверы типов
|
||
Organization: {
|
||
users: async (parent: { id: string; users?: unknown[] }) => {
|
||
// Если пользователи уже загружены через include, возвращаем их
|
||
if (parent.users) {
|
||
return parent.users
|
||
}
|
||
|
||
// Иначе загружаем отдельно
|
||
return await prisma.user.findMany({
|
||
where: { organizationId: parent.id },
|
||
})
|
||
},
|
||
services: async (parent: { id: string; services?: unknown[] }) => {
|
||
// Если услуги уже загружены через include, возвращаем их
|
||
if (parent.services) {
|
||
return parent.services
|
||
}
|
||
|
||
// Иначе загружаем отдельно
|
||
return await prisma.service.findMany({
|
||
where: { organizationId: parent.id },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
supplies: async (parent: { id: string; supplies?: unknown[] }) => {
|
||
// Если расходники уже загружены через include, возвращаем их
|
||
if (parent.supplies) {
|
||
return parent.supplies
|
||
}
|
||
|
||
// Иначе загружаем отдельно
|
||
return await prisma.supply.findMany({
|
||
where: { organizationId: parent.id },
|
||
include: {
|
||
organization: true,
|
||
sellerOwner: true, // Включаем информацию о селлере-владельце
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
},
|
||
},
|
||
|
||
Cart: {
|
||
totalPrice: (parent: { items: Array<{ product: { price: number }; quantity: number }> }) => {
|
||
return parent.items.reduce((total, item) => {
|
||
return total + Number(item.product.price) * item.quantity
|
||
}, 0)
|
||
},
|
||
totalItems: (parent: { items: Array<{ quantity: number }> }) => {
|
||
return parent.items.reduce((total, item) => total + item.quantity, 0)
|
||
},
|
||
},
|
||
|
||
CartItem: {
|
||
totalPrice: (parent: { product: { price: number }; quantity: number }) => {
|
||
return Number(parent.product.price) * parent.quantity
|
||
},
|
||
isAvailable: (parent: { product: { quantity: number; isActive: boolean }; quantity: number }) => {
|
||
return parent.product.isActive && parent.product.quantity >= parent.quantity
|
||
},
|
||
availableQuantity: (parent: { product: { quantity: number } }) => {
|
||
return parent.product.quantity
|
||
},
|
||
},
|
||
|
||
User: {
|
||
organization: async (parent: { organizationId?: string; organization?: unknown }) => {
|
||
// Если организация уже загружена через include, возвращаем её
|
||
if (parent.organization) {
|
||
return parent.organization
|
||
}
|
||
|
||
// Иначе загружаем отдельно если есть organizationId
|
||
if (parent.organizationId) {
|
||
return await prisma.organization.findUnique({
|
||
where: { id: parent.organizationId },
|
||
include: {
|
||
apiKeys: true,
|
||
users: true,
|
||
},
|
||
})
|
||
}
|
||
|
||
return null
|
||
},
|
||
},
|
||
|
||
Product: {
|
||
type: (parent: { type?: string | null }) => parent.type || 'PRODUCT',
|
||
images: (parent: { images: unknown }) => {
|
||
// Если images это строка JSON, парсим её в массив
|
||
if (typeof parent.images === 'string') {
|
||
try {
|
||
return JSON.parse(parent.images)
|
||
} catch {
|
||
return []
|
||
}
|
||
}
|
||
// Если это уже массив, возвращаем как есть
|
||
if (Array.isArray(parent.images)) {
|
||
return parent.images
|
||
}
|
||
// Иначе возвращаем пустой массив
|
||
return []
|
||
},
|
||
},
|
||
|
||
Message: {
|
||
type: (parent: { type?: string | null }) => {
|
||
return parent.type || 'TEXT'
|
||
},
|
||
createdAt: (parent: { createdAt: Date | string }) => {
|
||
if (parent.createdAt instanceof Date) {
|
||
return parent.createdAt.toISOString()
|
||
}
|
||
return parent.createdAt
|
||
},
|
||
updatedAt: (parent: { updatedAt: Date | string }) => {
|
||
if (parent.updatedAt instanceof Date) {
|
||
return parent.updatedAt.toISOString()
|
||
}
|
||
return parent.updatedAt
|
||
},
|
||
},
|
||
|
||
Employee: {
|
||
fullName: (parent: { firstName: string; lastName: string; middleName?: string }) => {
|
||
const parts = [parent.lastName, parent.firstName]
|
||
if (parent.middleName) {
|
||
parts.push(parent.middleName)
|
||
}
|
||
return parts.join(' ')
|
||
},
|
||
name: (parent: { firstName: string; lastName: string }) => {
|
||
return `${parent.firstName} ${parent.lastName}`
|
||
},
|
||
birthDate: (parent: { birthDate?: Date | string | null }) => {
|
||
if (!parent.birthDate) return null
|
||
if (parent.birthDate instanceof Date) {
|
||
return parent.birthDate.toISOString()
|
||
}
|
||
return parent.birthDate
|
||
},
|
||
passportDate: (parent: { passportDate?: Date | string | null }) => {
|
||
if (!parent.passportDate) return null
|
||
if (parent.passportDate instanceof Date) {
|
||
return parent.passportDate.toISOString()
|
||
}
|
||
return parent.passportDate
|
||
},
|
||
hireDate: (parent: { hireDate: Date | string }) => {
|
||
if (parent.hireDate instanceof Date) {
|
||
return parent.hireDate.toISOString()
|
||
}
|
||
return parent.hireDate
|
||
},
|
||
createdAt: (parent: { createdAt: Date | string }) => {
|
||
if (parent.createdAt instanceof Date) {
|
||
return parent.createdAt.toISOString()
|
||
}
|
||
return parent.createdAt
|
||
},
|
||
updatedAt: (parent: { updatedAt: Date | string }) => {
|
||
if (parent.updatedAt instanceof Date) {
|
||
return parent.updatedAt.toISOString()
|
||
}
|
||
return parent.updatedAt
|
||
},
|
||
},
|
||
|
||
EmployeeSchedule: {
|
||
date: (parent: { date: Date | string }) => {
|
||
if (parent.date instanceof Date) {
|
||
return parent.date.toISOString()
|
||
}
|
||
return parent.date
|
||
},
|
||
createdAt: (parent: { createdAt: Date | string }) => {
|
||
if (parent.createdAt instanceof Date) {
|
||
return parent.createdAt.toISOString()
|
||
}
|
||
return parent.createdAt
|
||
},
|
||
updatedAt: (parent: { updatedAt: Date | string }) => {
|
||
if (parent.updatedAt instanceof Date) {
|
||
return parent.updatedAt.toISOString()
|
||
}
|
||
return parent.updatedAt
|
||
},
|
||
employee: async (parent: { employeeId: string }) => {
|
||
return await prisma.employee.findUnique({
|
||
where: { id: parent.employeeId },
|
||
})
|
||
},
|
||
},
|
||
}
|
||
|
||
// Мутации для категорий
|
||
const categoriesMutations = {
|
||
// Создать категорию
|
||
createCategory: async (_: unknown, args: { input: { name: string } }) => {
|
||
try {
|
||
// Проверяем есть ли уже категория с таким именем
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { name: args.input.name },
|
||
})
|
||
|
||
if (existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория с таким названием уже существует',
|
||
}
|
||
}
|
||
|
||
const category = await prisma.category.create({
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Категория успешно создана',
|
||
category,
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка создания категории:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании категории',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить категорию
|
||
updateCategory: async (_: unknown, args: { id: string; input: { name: string } }) => {
|
||
try {
|
||
// Проверяем существует ли категория
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
if (!existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория не найдена',
|
||
}
|
||
}
|
||
|
||
// Проверяем не занято ли имя другой категорией
|
||
const duplicateCategory = await prisma.category.findFirst({
|
||
where: {
|
||
name: args.input.name,
|
||
id: { not: args.id },
|
||
},
|
||
})
|
||
|
||
if (duplicateCategory) {
|
||
return {
|
||
success: false,
|
||
message: 'Категория с таким названием уже существует',
|
||
}
|
||
}
|
||
|
||
const category = await prisma.category.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Категория успешно обновлена',
|
||
category,
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка обновления категории:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении категории',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить категорию
|
||
deleteCategory: async (_: unknown, args: { id: string }) => {
|
||
try {
|
||
// Проверяем существует ли категория
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
if (!existingCategory) {
|
||
throw new GraphQLError('Категория не найдена')
|
||
}
|
||
|
||
// Проверяем есть ли товары в этой категории
|
||
const productsCount = await prisma.product.count({
|
||
where: { categoryId: args.id },
|
||
})
|
||
|
||
if (productsCount > 0) {
|
||
throw new GraphQLError('Нельзя удалить категорию, в которой есть товары')
|
||
}
|
||
|
||
await prisma.category.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('Ошибка удаления категории:', error)
|
||
if (error instanceof GraphQLError) {
|
||
throw error
|
||
}
|
||
throw new GraphQLError('Ошибка при удалении категории')
|
||
}
|
||
},
|
||
}
|
||
|
||
// Логистические мутации
|
||
const logisticsMutations = {
|
||
// Создать логистический маршрут
|
||
createLogistics: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
fromLocation: string
|
||
toLocation: string
|
||
priceUnder1m3: number
|
||
priceOver1m3: number
|
||
description?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
const logistics = await prisma.logistics.create({
|
||
data: {
|
||
fromLocation: args.input.fromLocation,
|
||
toLocation: args.input.toLocation,
|
||
priceUnder1m3: args.input.priceUnder1m3,
|
||
priceOver1m3: args.input.priceOver1m3,
|
||
description: args.input.description,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
console.warn('✅ Logistics created:', logistics.id)
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Логистический маршрут создан',
|
||
logistics,
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error creating logistics:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании логистического маршрута',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить логистический маршрут
|
||
updateLogistics: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
input: {
|
||
fromLocation: string
|
||
toLocation: string
|
||
priceUnder1m3: number
|
||
priceOver1m3: number
|
||
description?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что маршрут принадлежит организации пользователя
|
||
const existingLogistics = await prisma.logistics.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingLogistics) {
|
||
throw new GraphQLError('Логистический маршрут не найден')
|
||
}
|
||
|
||
const logistics = await prisma.logistics.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
fromLocation: args.input.fromLocation,
|
||
toLocation: args.input.toLocation,
|
||
priceUnder1m3: args.input.priceUnder1m3,
|
||
priceOver1m3: args.input.priceOver1m3,
|
||
description: args.input.description,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
console.warn('✅ Logistics updated:', logistics.id)
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Логистический маршрут обновлен',
|
||
logistics,
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error updating logistics:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении логистического маршрута',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить логистический маршрут
|
||
deleteLogistics: async (_: unknown, args: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что маршрут принадлежит организации пользователя
|
||
const existingLogistics = await prisma.logistics.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingLogistics) {
|
||
throw new GraphQLError('Логистический маршрут не найден')
|
||
}
|
||
|
||
await prisma.logistics.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
console.warn('✅ Logistics deleted:', args.id)
|
||
return true
|
||
} catch (error) {
|
||
console.error('❌ Error deleting logistics:', error)
|
||
return false
|
||
}
|
||
},
|
||
}
|
||
|
||
// Добавляем дополнительные мутации к основным резолверам
|
||
resolvers.Mutation = {
|
||
...resolvers.Mutation,
|
||
...categoriesMutations,
|
||
...logisticsMutations,
|
||
}
|
||
|
||
// Админ резолверы
|
||
const adminQueries = {
|
||
adminMe: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.admin) {
|
||
throw new GraphQLError('Требуется авторизация администратора', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const admin = await prisma.admin.findUnique({
|
||
where: { id: context.admin.id },
|
||
})
|
||
|
||
if (!admin) {
|
||
throw new GraphQLError('Администратор не найден')
|
||
}
|
||
|
||
return admin
|
||
},
|
||
|
||
allUsers: async (_: unknown, args: { search?: string; limit?: number; offset?: number }, context: Context) => {
|
||
if (!context.admin) {
|
||
throw new GraphQLError('Требуется авторизация администратора', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const limit = args.limit || 50
|
||
const offset = args.offset || 0
|
||
|
||
// Строим условие поиска
|
||
const whereCondition: Prisma.UserWhereInput = args.search
|
||
? {
|
||
OR: [
|
||
{ phone: { contains: args.search, mode: 'insensitive' } },
|
||
{ managerName: { contains: args.search, mode: 'insensitive' } },
|
||
{
|
||
organization: {
|
||
OR: [
|
||
{ name: { contains: args.search, mode: 'insensitive' } },
|
||
{ fullName: { contains: args.search, mode: 'insensitive' } },
|
||
{ inn: { contains: args.search, mode: 'insensitive' } },
|
||
],
|
||
},
|
||
},
|
||
],
|
||
}
|
||
: {}
|
||
|
||
// Получаем пользователей с пагинацией
|
||
const [users, total] = await Promise.all([
|
||
prisma.user.findMany({
|
||
where: whereCondition,
|
||
include: {
|
||
organization: true,
|
||
},
|
||
take: limit,
|
||
skip: offset,
|
||
orderBy: { createdAt: 'desc' },
|
||
}),
|
||
prisma.user.count({
|
||
where: whereCondition,
|
||
}),
|
||
])
|
||
|
||
return {
|
||
users,
|
||
total,
|
||
hasMore: offset + limit < total,
|
||
}
|
||
},
|
||
}
|
||
|
||
const adminMutations = {
|
||
adminLogin: async (_: unknown, args: { username: string; password: string }) => {
|
||
try {
|
||
// Найти администратора
|
||
const admin = await prisma.admin.findUnique({
|
||
where: { username: args.username },
|
||
})
|
||
|
||
if (!admin) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверные учетные данные',
|
||
}
|
||
}
|
||
|
||
// Проверить активность
|
||
if (!admin.isActive) {
|
||
return {
|
||
success: false,
|
||
message: 'Аккаунт заблокирован',
|
||
}
|
||
}
|
||
|
||
// Проверить пароль
|
||
const isPasswordValid = await bcrypt.compare(args.password, admin.password)
|
||
|
||
if (!isPasswordValid) {
|
||
return {
|
||
success: false,
|
||
message: 'Неверные учетные данные',
|
||
}
|
||
}
|
||
|
||
// Обновить время последнего входа
|
||
await prisma.admin.update({
|
||
where: { id: admin.id },
|
||
data: { lastLogin: new Date() },
|
||
})
|
||
|
||
// Создать токен
|
||
const token = jwt.sign(
|
||
{
|
||
adminId: admin.id,
|
||
username: admin.username,
|
||
type: 'admin',
|
||
},
|
||
process.env.JWT_SECRET!,
|
||
{ expiresIn: '24h' },
|
||
)
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Успешная авторизация',
|
||
token,
|
||
admin: {
|
||
...admin,
|
||
password: undefined, // Не возвращаем пароль
|
||
},
|
||
}
|
||
} catch (error) {
|
||
console.error('Admin login error:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка авторизации',
|
||
}
|
||
}
|
||
},
|
||
|
||
adminLogout: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.admin) {
|
||
throw new GraphQLError('Требуется авторизация администратора', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
return true
|
||
},
|
||
}
|
||
|
||
// Wildberries статистика
|
||
const wildberriesQueries = {
|
||
debugWildberriesAdverts: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization || user.organization.type !== 'SELLER') {
|
||
throw new GraphQLError('Доступно только для продавцов')
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError('WB API ключ не настроен')
|
||
}
|
||
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
|
||
|
||
// Получаем кампании во всех статусах
|
||
const [active, completed, paused] = await Promise.all([
|
||
wbService.getAdverts(9).catch(() => []), // активные
|
||
wbService.getAdverts(7).catch(() => []), // завершенные
|
||
wbService.getAdverts(11).catch(() => []), // на паузе
|
||
])
|
||
|
||
const allCampaigns = [...active, ...completed, ...paused]
|
||
|
||
return {
|
||
success: true,
|
||
message: `Found ${active.length} active, ${completed.length} completed, ${paused.length} paused campaigns`,
|
||
campaignsCount: allCampaigns.length,
|
||
campaigns: allCampaigns.map((c) => ({
|
||
id: c.advertId,
|
||
name: c.name,
|
||
status: c.status,
|
||
type: c.type,
|
||
})),
|
||
}
|
||
} catch (error) {
|
||
console.error('Error debugging WB adverts:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Unknown error',
|
||
campaignsCount: 0,
|
||
campaigns: [],
|
||
}
|
||
}
|
||
},
|
||
|
||
getWildberriesStatistics: async (
|
||
_: unknown,
|
||
{
|
||
period,
|
||
startDate,
|
||
endDate,
|
||
}: {
|
||
period?: 'week' | 'month' | 'quarter'
|
||
startDate?: string
|
||
endDate?: string
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Получаем организацию пользователя и её WB API ключ
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
if (user.organization.type !== 'SELLER') {
|
||
throw new GraphQLError('Доступно только для продавцов')
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError('WB API ключ не настроен')
|
||
}
|
||
|
||
// Создаем экземпляр сервиса
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
|
||
|
||
// Получаем даты
|
||
let dateFrom: string
|
||
let dateTo: string
|
||
|
||
if (startDate && endDate) {
|
||
// Используем пользовательские даты
|
||
dateFrom = startDate
|
||
dateTo = endDate
|
||
} else if (period) {
|
||
// Используем предустановленный период
|
||
dateFrom = WildberriesService.getDatePeriodAgo(period)
|
||
dateTo = WildberriesService.formatDate(new Date())
|
||
} else {
|
||
throw new GraphQLError('Необходимо указать либо period, либо startDate и endDate')
|
||
}
|
||
|
||
// Получаем статистику
|
||
const statistics = await wbService.getStatistics(dateFrom, dateTo)
|
||
|
||
return {
|
||
success: true,
|
||
data: statistics,
|
||
message: null,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching WB statistics:', error)
|
||
// Фолбэк: пробуем вернуть последние данные из кеша статистики селлера
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user!.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (user?.organization) {
|
||
const whereCache: any = {
|
||
organizationId: user.organization.id,
|
||
period: startDate && endDate ? 'custom' : (period ?? 'week'),
|
||
}
|
||
if (startDate && endDate) {
|
||
whereCache.dateFrom = new Date(startDate)
|
||
whereCache.dateTo = new Date(endDate)
|
||
}
|
||
|
||
const cache = await prisma.sellerStatsCache.findFirst({
|
||
where: whereCache,
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
if (cache?.productsData) {
|
||
// Ожидаем, что productsData — строка JSON с полями, сохраненными клиентом
|
||
const parsed = JSON.parse(cache.productsData as unknown as string) as {
|
||
tableData?: Array<{
|
||
date: string
|
||
salesUnits: number
|
||
orders: number
|
||
advertising: number
|
||
refusals: number
|
||
returns: number
|
||
revenue: number
|
||
buyoutPercentage: number
|
||
}>
|
||
}
|
||
|
||
const table = parsed.tableData ?? []
|
||
const dataFromCache = table.map((row) => ({
|
||
date: row.date,
|
||
sales: row.salesUnits,
|
||
orders: row.orders,
|
||
advertising: row.advertising,
|
||
refusals: row.refusals,
|
||
returns: row.returns,
|
||
revenue: row.revenue,
|
||
buyoutPercentage: row.buyoutPercentage,
|
||
}))
|
||
|
||
if (dataFromCache.length > 0) {
|
||
return {
|
||
success: true,
|
||
data: dataFromCache,
|
||
message: 'Данные возвращены из кеша из-за ошибки WB API',
|
||
}
|
||
}
|
||
} else if (cache?.advertisingData) {
|
||
// Fallback №2: если нет productsData, но есть advertisingData —
|
||
// формируем минимальный набор данных по дням на основе затрат на рекламу
|
||
try {
|
||
const adv = JSON.parse(cache.advertisingData as unknown as string) as {
|
||
dailyData?: Array<{
|
||
date: string
|
||
totalSum?: number
|
||
totalOrders?: number
|
||
totalRevenue?: number
|
||
}>
|
||
}
|
||
|
||
const daily = adv.dailyData ?? []
|
||
const dataFromAdv = daily.map((d) => ({
|
||
date: d.date,
|
||
sales: 0,
|
||
orders: typeof d.totalOrders === 'number' ? d.totalOrders : 0,
|
||
advertising: typeof d.totalSum === 'number' ? d.totalSum : 0,
|
||
refusals: 0,
|
||
returns: 0,
|
||
revenue: typeof d.totalRevenue === 'number' ? d.totalRevenue : 0,
|
||
buyoutPercentage: 0,
|
||
}))
|
||
|
||
if (dataFromAdv.length > 0) {
|
||
return {
|
||
success: true,
|
||
data: dataFromAdv,
|
||
message: 'Данные по продажам недоступны из-за ошибки WB API. Показаны данные по рекламе из кеша.',
|
||
}
|
||
}
|
||
} catch (parseErr) {
|
||
console.error('Failed to parse advertisingData from cache:', parseErr)
|
||
}
|
||
}
|
||
}
|
||
} catch (fallbackErr) {
|
||
console.error('Seller stats cache fallback failed:', fallbackErr)
|
||
}
|
||
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения статистики',
|
||
data: [],
|
||
}
|
||
}
|
||
},
|
||
|
||
getWildberriesCampaignStats: async (
|
||
_: unknown,
|
||
{
|
||
input,
|
||
}: {
|
||
input: {
|
||
campaigns: Array<{
|
||
id: number
|
||
dates?: string[]
|
||
interval?: {
|
||
begin: string
|
||
end: string
|
||
}
|
||
}>
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Получаем организацию пользователя и её WB API ключ
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
if (user.organization.type !== 'SELLER') {
|
||
throw new GraphQLError('Доступно только для продавцов')
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError('WB API ключ не настроен')
|
||
}
|
||
|
||
// Создаем экземпляр сервиса
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
|
||
|
||
// Преобразуем запросы в нужный формат
|
||
const requests = input.campaigns.map((campaign) => {
|
||
if (campaign.dates && campaign.dates.length > 0) {
|
||
return {
|
||
id: campaign.id,
|
||
dates: campaign.dates,
|
||
}
|
||
} else if (campaign.interval) {
|
||
return {
|
||
id: campaign.id,
|
||
interval: campaign.interval,
|
||
}
|
||
} else {
|
||
// Если не указаны ни даты, ни интервал, возвращаем данные только за последние сутки
|
||
return {
|
||
id: campaign.id,
|
||
}
|
||
}
|
||
})
|
||
|
||
// Получаем статистику кампаний
|
||
const campaignStats = await wbService.getCampaignStats(requests)
|
||
|
||
return {
|
||
success: true,
|
||
data: campaignStats,
|
||
message: null,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching WB campaign stats:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения статистики кампаний',
|
||
data: [],
|
||
}
|
||
}
|
||
},
|
||
|
||
getWildberriesCampaignsList: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Получаем организацию пользователя и её WB API ключ
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
if (user.organization.type !== 'SELLER') {
|
||
throw new GraphQLError('Доступно только для продавцов')
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError('WB API ключ не настроен')
|
||
}
|
||
|
||
// Создаем экземпляр сервиса
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
|
||
|
||
// Получаем список кампаний
|
||
const campaignsList = await wbService.getCampaignsList()
|
||
|
||
return {
|
||
success: true,
|
||
data: campaignsList,
|
||
message: null,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching WB campaigns list:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения списка кампаний',
|
||
data: {
|
||
adverts: [],
|
||
all: 0,
|
||
},
|
||
}
|
||
}
|
||
},
|
||
|
||
// Получение заявок покупателей на возврат от Wildberries от всех партнеров-селлеров
|
||
wbReturnClaims: async (
|
||
_: unknown,
|
||
{ isArchive, limit, offset }: { isArchive: boolean; limit?: number; offset?: number },
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
// Получаем текущую организацию пользователя (фулфилмент)
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: true,
|
||
},
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент организация
|
||
if (user.organization.type !== 'FULFILLMENT') {
|
||
throw new GraphQLError('Доступ только для фулфилмент организаций')
|
||
}
|
||
|
||
// Получаем всех партнеров-селлеров с активными WB API ключами
|
||
const partnerSellerOrgs = await prisma.counterparty.findMany({
|
||
where: {
|
||
organizationId: user.organization.id,
|
||
},
|
||
include: {
|
||
counterparty: {
|
||
include: {
|
||
apiKeys: {
|
||
where: {
|
||
marketplace: 'WILDBERRIES',
|
||
isActive: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
// Фильтруем только селлеров с WB API ключами
|
||
const sellersWithWbKeys = partnerSellerOrgs.filter(
|
||
(partner) => partner.counterparty.type === 'SELLER' && partner.counterparty.apiKeys.length > 0,
|
||
)
|
||
|
||
if (sellersWithWbKeys.length === 0) {
|
||
return {
|
||
claims: [],
|
||
total: 0,
|
||
}
|
||
}
|
||
|
||
console.warn(`Found ${sellersWithWbKeys.length} seller partners with WB keys`)
|
||
|
||
// Получаем заявки от всех селлеров параллельно
|
||
const claimsPromises = sellersWithWbKeys.map(async (partner) => {
|
||
const wbApiKey = partner.counterparty.apiKeys[0].apiKey
|
||
const wbService = new WildberriesService(wbApiKey)
|
||
|
||
try {
|
||
const claimsResponse = await wbService.getClaims({
|
||
isArchive,
|
||
limit: Math.ceil((limit || 50) / sellersWithWbKeys.length), // Распределяем лимит между селлерами
|
||
offset: 0,
|
||
})
|
||
|
||
// Добавляем информацию о селлере к каждой заявке
|
||
const claimsWithSeller = claimsResponse.claims.map((claim) => ({
|
||
...claim,
|
||
sellerOrganization: {
|
||
id: partner.counterparty.id,
|
||
name: partner.counterparty.name || 'Неизвестная организация',
|
||
inn: partner.counterparty.inn || '',
|
||
},
|
||
}))
|
||
|
||
console.warn(`Got ${claimsWithSeller.length} claims from seller ${partner.counterparty.name}`)
|
||
return claimsWithSeller
|
||
} catch (error) {
|
||
console.error(`Error fetching claims for seller ${partner.counterparty.name}:`, error)
|
||
return []
|
||
}
|
||
})
|
||
|
||
const allClaims = (await Promise.all(claimsPromises)).flat()
|
||
console.warn(`Total claims aggregated: ${allClaims.length}`)
|
||
|
||
// Сортируем по дате создания (новые первыми)
|
||
allClaims.sort((a, b) => new Date(b.dt).getTime() - new Date(a.dt).getTime())
|
||
|
||
// Применяем пагинацию
|
||
const paginatedClaims = allClaims.slice(offset || 0, (offset || 0) + (limit || 50))
|
||
console.warn(`Paginated claims: ${paginatedClaims.length}`)
|
||
|
||
// Преобразуем в формат фронтенда
|
||
const transformedClaims = paginatedClaims.map((claim) => ({
|
||
id: claim.id,
|
||
claimType: claim.claim_type,
|
||
status: claim.status,
|
||
statusEx: claim.status_ex,
|
||
nmId: claim.nm_id,
|
||
userComment: claim.user_comment || '',
|
||
wbComment: claim.wb_comment || null,
|
||
dt: claim.dt,
|
||
imtName: claim.imt_name,
|
||
orderDt: claim.order_dt,
|
||
dtUpdate: claim.dt_update,
|
||
photos: claim.photos || [],
|
||
videoPaths: claim.video_paths || [],
|
||
actions: claim.actions || [],
|
||
price: claim.price,
|
||
currencyCode: claim.currency_code,
|
||
srid: claim.srid,
|
||
sellerOrganization: claim.sellerOrganization,
|
||
}))
|
||
|
||
console.warn(`Returning ${transformedClaims.length} transformed claims to frontend`)
|
||
|
||
return {
|
||
claims: transformedClaims,
|
||
total: allClaims.length,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching WB return claims:', error)
|
||
throw new GraphQLError(error instanceof Error ? error.message : 'Ошибка получения заявок на возврат')
|
||
}
|
||
},
|
||
}
|
||
|
||
// Резолверы для внешней рекламы
|
||
const externalAdQueries = {
|
||
getExternalAds: async (_: unknown, { dateFrom, dateTo }: { dateFrom: string; dateTo: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
const externalAds = await prisma.externalAd.findMany({
|
||
where: {
|
||
organizationId: user.organization.id,
|
||
date: {
|
||
gte: new Date(dateFrom),
|
||
lte: new Date(dateTo + 'T23:59:59.999Z'),
|
||
},
|
||
},
|
||
orderBy: {
|
||
date: 'desc',
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: null,
|
||
externalAds: externalAds.map((ad) => ({
|
||
...ad,
|
||
cost: parseFloat(ad.cost.toString()),
|
||
date: ad.date.toISOString().split('T')[0],
|
||
createdAt: ad.createdAt.toISOString(),
|
||
updatedAt: ad.updatedAt.toISOString(),
|
||
})),
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching external ads:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения внешней рекламы',
|
||
externalAds: [],
|
||
}
|
||
}
|
||
},
|
||
}
|
||
|
||
const externalAdMutations = {
|
||
createExternalAd: async (
|
||
_: unknown,
|
||
{
|
||
input,
|
||
}: {
|
||
input: {
|
||
name: string
|
||
url: string
|
||
cost: number
|
||
date: string
|
||
nmId: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
const externalAd = await prisma.externalAd.create({
|
||
data: {
|
||
name: input.name,
|
||
url: input.url,
|
||
cost: input.cost,
|
||
date: new Date(input.date),
|
||
nmId: input.nmId,
|
||
organizationId: user.organization.id,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Внешняя реклама успешно создана',
|
||
externalAd: {
|
||
...externalAd,
|
||
cost: parseFloat(externalAd.cost.toString()),
|
||
date: externalAd.date.toISOString().split('T')[0],
|
||
createdAt: externalAd.createdAt.toISOString(),
|
||
updatedAt: externalAd.updatedAt.toISOString(),
|
||
},
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating external ad:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка создания внешней рекламы',
|
||
externalAd: null,
|
||
}
|
||
}
|
||
},
|
||
|
||
updateExternalAd: async (
|
||
_: unknown,
|
||
{
|
||
id,
|
||
input,
|
||
}: {
|
||
id: string
|
||
input: {
|
||
name: string
|
||
url: string
|
||
cost: number
|
||
date: string
|
||
nmId: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Проверяем, что реклама принадлежит организации пользователя
|
||
const existingAd = await prisma.externalAd.findFirst({
|
||
where: {
|
||
id,
|
||
organizationId: user.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingAd) {
|
||
throw new GraphQLError('Внешняя реклама не найдена')
|
||
}
|
||
|
||
const externalAd = await prisma.externalAd.update({
|
||
where: { id },
|
||
data: {
|
||
name: input.name,
|
||
url: input.url,
|
||
cost: input.cost,
|
||
date: new Date(input.date),
|
||
nmId: input.nmId,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Внешняя реклама успешно обновлена',
|
||
externalAd: {
|
||
...externalAd,
|
||
cost: parseFloat(externalAd.cost.toString()),
|
||
date: externalAd.date.toISOString().split('T')[0],
|
||
createdAt: externalAd.createdAt.toISOString(),
|
||
updatedAt: externalAd.updatedAt.toISOString(),
|
||
},
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating external ad:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка обновления внешней рекламы',
|
||
externalAd: null,
|
||
}
|
||
}
|
||
},
|
||
|
||
deleteExternalAd: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Проверяем, что реклама принадлежит организации пользователя
|
||
const existingAd = await prisma.externalAd.findFirst({
|
||
where: {
|
||
id,
|
||
organizationId: user.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingAd) {
|
||
throw new GraphQLError('Внешняя реклама не найдена')
|
||
}
|
||
|
||
await prisma.externalAd.delete({
|
||
where: { id },
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Внешняя реклама успешно удалена',
|
||
externalAd: null,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting external ad:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка удаления внешней рекламы',
|
||
externalAd: null,
|
||
}
|
||
}
|
||
},
|
||
}
|
||
|
||
// Резолверы для кеша склада WB
|
||
const wbWarehouseCacheQueries = {
|
||
getWBWarehouseData: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Получаем текущую дату без времени
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
|
||
// Ищем кеш за сегодня
|
||
const cache = await prisma.wBWarehouseCache.findFirst({
|
||
where: {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
},
|
||
orderBy: {
|
||
createdAt: 'desc',
|
||
},
|
||
})
|
||
|
||
if (cache) {
|
||
// Возвращаем данные из кеша
|
||
return {
|
||
success: true,
|
||
message: 'Данные получены из кеша',
|
||
cache: {
|
||
...cache,
|
||
cacheDate: cache.cacheDate.toISOString().split('T')[0],
|
||
createdAt: cache.createdAt.toISOString(),
|
||
updatedAt: cache.updatedAt.toISOString(),
|
||
},
|
||
fromCache: true,
|
||
}
|
||
} else {
|
||
// Кеша нет, нужно загрузить данные из API
|
||
return {
|
||
success: true,
|
||
message: 'Кеш не найден, требуется загрузка из API',
|
||
cache: null,
|
||
fromCache: false,
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error getting WB warehouse cache:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения кеша склада WB',
|
||
cache: null,
|
||
fromCache: false,
|
||
}
|
||
}
|
||
},
|
||
}
|
||
|
||
const wbWarehouseCacheMutations = {
|
||
saveWBWarehouseCache: async (
|
||
_: unknown,
|
||
{
|
||
input,
|
||
}: {
|
||
input: {
|
||
data: string
|
||
totalProducts: number
|
||
totalStocks: number
|
||
totalReserved: number
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
// Получаем текущую дату без времени
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
|
||
// Используем upsert для создания или обновления кеша
|
||
const cache = await prisma.wBWarehouseCache.upsert({
|
||
where: {
|
||
organizationId_cacheDate: {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
},
|
||
},
|
||
update: {
|
||
data: input.data,
|
||
totalProducts: input.totalProducts,
|
||
totalStocks: input.totalStocks,
|
||
totalReserved: input.totalReserved,
|
||
},
|
||
create: {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
data: input.data,
|
||
totalProducts: input.totalProducts,
|
||
totalStocks: input.totalStocks,
|
||
totalReserved: input.totalReserved,
|
||
},
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Кеш склада WB успешно сохранен',
|
||
cache: {
|
||
...cache,
|
||
cacheDate: cache.cacheDate.toISOString().split('T')[0],
|
||
createdAt: cache.createdAt.toISOString(),
|
||
updatedAt: cache.updatedAt.toISOString(),
|
||
},
|
||
fromCache: false,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving WB warehouse cache:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка сохранения кеша склада WB',
|
||
cache: null,
|
||
fromCache: false,
|
||
}
|
||
}
|
||
},
|
||
}
|
||
|
||
// Добавляем админ запросы и мутации к основным резолверам
|
||
resolvers.Query = {
|
||
...resolvers.Query,
|
||
...adminQueries,
|
||
...wildberriesQueries,
|
||
...externalAdQueries,
|
||
...wbWarehouseCacheQueries,
|
||
// Кеш статистики селлера
|
||
getSellerStatsCache: async (
|
||
_: unknown,
|
||
args: { period: string; dateFrom?: string | null; dateTo?: string | null },
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
|
||
// Для custom учитываем диапазон, иначе только period
|
||
const where: any = {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
period: args.period,
|
||
}
|
||
if (args.period === 'custom') {
|
||
if (!args.dateFrom || !args.dateTo) {
|
||
throw new GraphQLError('Для custom необходимо указать dateFrom и dateTo')
|
||
}
|
||
where.dateFrom = new Date(args.dateFrom)
|
||
where.dateTo = new Date(args.dateTo)
|
||
}
|
||
|
||
const cache = await prisma.sellerStatsCache.findFirst({
|
||
where,
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
if (!cache) {
|
||
return {
|
||
success: true,
|
||
message: 'Кеш не найден',
|
||
cache: null,
|
||
fromCache: false,
|
||
}
|
||
}
|
||
|
||
// Если кеш просрочен — не используем его, как и для склада WB (сервер решает, годен ли кеш)
|
||
const now = new Date()
|
||
if (cache.expiresAt && cache.expiresAt <= now) {
|
||
return {
|
||
success: true,
|
||
message: 'Кеш устарел, требуется загрузка из API',
|
||
cache: null,
|
||
fromCache: false,
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Данные получены из кеша',
|
||
cache: {
|
||
...cache,
|
||
cacheDate: cache.cacheDate.toISOString().split('T')[0],
|
||
dateFrom: cache.dateFrom ? cache.dateFrom.toISOString().split('T')[0] : null,
|
||
dateTo: cache.dateTo ? cache.dateTo.toISOString().split('T')[0] : null,
|
||
productsTotalSales: cache.productsTotalSales ? Number(cache.productsTotalSales) : null,
|
||
advertisingTotalCost: cache.advertisingTotalCost ? Number(cache.advertisingTotalCost) : null,
|
||
// Возвращаем expiresAt в ISO, чтобы клиент корректно парсил дату
|
||
expiresAt: cache.expiresAt.toISOString(),
|
||
createdAt: cache.createdAt.toISOString(),
|
||
updatedAt: cache.updatedAt.toISOString(),
|
||
},
|
||
fromCache: true,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error getting Seller Stats cache:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка получения кеша статистики',
|
||
cache: null,
|
||
fromCache: false,
|
||
}
|
||
}
|
||
},
|
||
}
|
||
|
||
resolvers.Mutation = {
|
||
...resolvers.Mutation,
|
||
...adminMutations,
|
||
...externalAdMutations,
|
||
...wbWarehouseCacheMutations,
|
||
// Сохранение кеша статистики селлера
|
||
saveSellerStatsCache: async (
|
||
_: unknown,
|
||
{
|
||
input,
|
||
}: {
|
||
input: {
|
||
period: string
|
||
dateFrom?: string | null
|
||
dateTo?: string | null
|
||
productsData?: string | null
|
||
productsTotalSales?: number | null
|
||
productsTotalOrders?: number | null
|
||
productsCount?: number | null
|
||
advertisingData?: string | null
|
||
advertisingTotalCost?: number | null
|
||
advertisingTotalViews?: number | null
|
||
advertisingTotalClicks?: number | null
|
||
expiresAt: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError('Организация не найдена')
|
||
}
|
||
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
|
||
const data: any = {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
period: input.period,
|
||
dateFrom: input.period === 'custom' && input.dateFrom ? new Date(input.dateFrom) : null,
|
||
dateTo: input.period === 'custom' && input.dateTo ? new Date(input.dateTo) : null,
|
||
productsData: input.productsData ?? null,
|
||
productsTotalSales: input.productsTotalSales ?? null,
|
||
productsTotalOrders: input.productsTotalOrders ?? null,
|
||
productsCount: input.productsCount ?? null,
|
||
advertisingData: input.advertisingData ?? null,
|
||
advertisingTotalCost: input.advertisingTotalCost ?? null,
|
||
advertisingTotalViews: input.advertisingTotalViews ?? null,
|
||
advertisingTotalClicks: input.advertisingTotalClicks ?? null,
|
||
expiresAt: new Date(input.expiresAt),
|
||
}
|
||
|
||
// upsert с составным уникальным ключом, содержащим NULL, в Prisma вызывает валидацию.
|
||
// Делаем вручную: findFirst по уникальному набору, затем update или create.
|
||
const existing = await prisma.sellerStatsCache.findFirst({
|
||
where: {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
period: input.period,
|
||
dateFrom: data.dateFrom,
|
||
dateTo: data.dateTo,
|
||
},
|
||
})
|
||
|
||
const cache = existing
|
||
? await prisma.sellerStatsCache.update({ where: { id: existing.id }, data })
|
||
: await prisma.sellerStatsCache.create({ data })
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Кеш статистики сохранен',
|
||
cache: {
|
||
...cache,
|
||
cacheDate: cache.cacheDate.toISOString().split('T')[0],
|
||
dateFrom: cache.dateFrom ? cache.dateFrom.toISOString().split('T')[0] : null,
|
||
dateTo: cache.dateTo ? cache.dateTo.toISOString().split('T')[0] : null,
|
||
productsTotalSales: cache.productsTotalSales ? Number(cache.productsTotalSales) : null,
|
||
advertisingTotalCost: cache.advertisingTotalCost ? Number(cache.advertisingTotalCost) : null,
|
||
createdAt: cache.createdAt.toISOString(),
|
||
updatedAt: cache.updatedAt.toISOString(),
|
||
},
|
||
fromCache: false,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving Seller Stats cache:', error)
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : 'Ошибка сохранения кеша статистики',
|
||
cache: null,
|
||
fromCache: false,
|
||
}
|
||
}
|
||
},
|
||
|
||
// Добавляем v2 mutations через spread
|
||
...fulfillmentConsumableV2Mutations
|
||
}
|
||
|
||
/* // Резолвер для парсинга JSON рецептуры в SupplyOrderItem
|
||
SupplyOrderItem: {
|
||
recipe: (parent: any) => {
|
||
// Если recipe это JSON строка, парсим её
|
||
if (typeof parent.recipe === 'string') {
|
||
try {
|
||
return JSON.parse(parent.recipe)
|
||
} catch (error) {
|
||
console.error('Error parsing recipe JSON:', error)
|
||
return null
|
||
}
|
||
}
|
||
// Если recipe уже объект, возвращаем как есть
|
||
return parent.recipe
|
||
},
|
||
},
|
||
*/
|
||
|
||
// ===============================================
|
||
// НОВАЯ СИСТЕМА ПОСТАВОК V2.0 - RESOLVERS
|
||
// ===============================================
|
||
|
||
export default resolvers
|