first commit
This commit is contained in:
7980
src/lib/graphql/resolvers.ts
Normal file
7980
src/lib/graphql/resolvers.ts
Normal file
@ -0,0 +1,7980 @@
|
||||
import { prisma } from '../prisma'
|
||||
import { SearchType } from '../../generated/prisma'
|
||||
import { createToken, comparePasswords, hashPassword } from '../auth'
|
||||
import { createAuditLog, AuditAction, getClientInfo } from '../audit'
|
||||
import { uploadBuffer, generateFileKey } from '../s3'
|
||||
import { smsService } from '../sms-service'
|
||||
import { smsCodeStore } from '../sms-code-store'
|
||||
import { laximoService, laximoDocService, laximoUnitService } from '../laximo-service'
|
||||
import { autoEuroService } from '../autoeuro-service'
|
||||
import { yooKassaService } from '../yookassa-service'
|
||||
import { partsAPIService } from '../partsapi-service'
|
||||
import { partsIndexService } from '../partsindex-service'
|
||||
import { yandexDeliveryService, YandexPickupPoint, getAddressSuggestions } from '../yandex-delivery-service'
|
||||
import { InvoiceService } from '../invoice-service'
|
||||
import * as csvWriter from 'csv-writer'
|
||||
import * as XLSX from 'xlsx'
|
||||
import GraphQLJSON from 'graphql-type-json'
|
||||
|
||||
interface CreateUserInput {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
password: string
|
||||
avatar?: string
|
||||
role?: 'ADMIN' | 'MODERATOR' | 'USER'
|
||||
}
|
||||
|
||||
interface LoginInput {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
interface UpdateProfileInput {
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email?: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
interface UpdateUserInput {
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email?: string
|
||||
avatar?: string
|
||||
role?: 'ADMIN' | 'MODERATOR' | 'USER'
|
||||
}
|
||||
|
||||
interface ChangePasswordInput {
|
||||
currentPassword: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
interface AdminChangePasswordInput {
|
||||
userId: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
interface Context {
|
||||
userId?: string
|
||||
clientId?: string
|
||||
userRole?: string
|
||||
userEmail?: string
|
||||
headers?: Headers
|
||||
}
|
||||
|
||||
// Функция для сохранения истории поиска запчастей и автомобилей
|
||||
const saveSearchHistory = async (
|
||||
context: Context,
|
||||
searchQuery: string,
|
||||
searchType: SearchType,
|
||||
brand?: string,
|
||||
articleNumber?: string,
|
||||
vehicleInfo?: { brand?: string; model?: string; year?: number },
|
||||
resultCount: number = 0
|
||||
) => {
|
||||
try {
|
||||
if (!context.clientId) {
|
||||
return // Не сохраняем историю для неавторизованных пользователей
|
||||
}
|
||||
|
||||
// Определяем clientId, убирая префикс client_ если он есть
|
||||
const clientIdParts = context.clientId.split('_')
|
||||
let clientId = context.clientId
|
||||
|
||||
if (clientIdParts.length >= 3) {
|
||||
clientId = clientIdParts[1] // client_ID_timestamp -> ID
|
||||
} else if (clientIdParts.length === 2) {
|
||||
clientId = clientIdParts[1] // client_ID -> ID
|
||||
}
|
||||
|
||||
// Проверяем существует ли клиент
|
||||
const client = await prisma.client.findUnique({
|
||||
where: { id: clientId }
|
||||
})
|
||||
|
||||
if (!client) {
|
||||
console.log('saveSearchHistory: клиент не найден:', clientId)
|
||||
return
|
||||
}
|
||||
|
||||
// Сохраняем в историю поиска
|
||||
await prisma.partsSearchHistory.create({
|
||||
data: {
|
||||
clientId,
|
||||
searchQuery,
|
||||
searchType,
|
||||
brand,
|
||||
articleNumber,
|
||||
vehicleBrand: vehicleInfo?.brand,
|
||||
vehicleModel: vehicleInfo?.model,
|
||||
vehicleYear: vehicleInfo?.year,
|
||||
resultCount
|
||||
}
|
||||
})
|
||||
|
||||
console.log('✅ Сохранена запись в истории поиска:', { searchQuery, searchType, resultCount })
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка сохранения истории поиска:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Интерфейсы для каталога
|
||||
interface CategoryInput {
|
||||
name: string
|
||||
slug?: string
|
||||
description?: string
|
||||
seoTitle?: string
|
||||
seoDescription?: string
|
||||
image?: string
|
||||
isHidden?: boolean
|
||||
includeSubcategoryProducts?: boolean
|
||||
parentId?: string
|
||||
}
|
||||
|
||||
interface ProductInput {
|
||||
name: string
|
||||
slug?: string
|
||||
article?: string
|
||||
description?: string
|
||||
videoUrl?: string
|
||||
wholesalePrice?: number
|
||||
retailPrice?: number
|
||||
weight?: number
|
||||
dimensions?: string
|
||||
unit?: string
|
||||
isVisible?: boolean
|
||||
applyDiscounts?: boolean
|
||||
stock?: number
|
||||
categoryIds?: string[]
|
||||
}
|
||||
|
||||
interface ProductImageInput {
|
||||
url: string
|
||||
alt?: string
|
||||
order?: number
|
||||
}
|
||||
|
||||
interface CharacteristicInput {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface ProductOptionInput {
|
||||
name: string
|
||||
type: 'SINGLE' | 'MULTIPLE'
|
||||
values: OptionValueInput[]
|
||||
}
|
||||
|
||||
interface OptionInput {
|
||||
name: string
|
||||
type: 'SINGLE' | 'MULTIPLE'
|
||||
values: OptionValueInput[]
|
||||
}
|
||||
|
||||
interface OptionValueInput {
|
||||
value: string
|
||||
price?: number
|
||||
}
|
||||
|
||||
// Интерфейсы для клиентов
|
||||
interface ClientInput {
|
||||
clientNumber?: string
|
||||
type: 'INDIVIDUAL' | 'LEGAL_ENTITY'
|
||||
name: string
|
||||
email?: string
|
||||
phone: string
|
||||
city?: string
|
||||
markup?: number
|
||||
isConfirmed?: boolean
|
||||
profileId?: string
|
||||
legalEntityType?: string
|
||||
inn?: string
|
||||
kpp?: string
|
||||
ogrn?: string
|
||||
okpo?: string
|
||||
legalAddress?: string
|
||||
actualAddress?: string
|
||||
bankAccount?: string
|
||||
bankName?: string
|
||||
bankBik?: string
|
||||
correspondentAccount?: string
|
||||
}
|
||||
|
||||
interface ClientProfileInput {
|
||||
code?: string
|
||||
name: string
|
||||
description?: string
|
||||
baseMarkup: number
|
||||
autoSendInvoice?: boolean
|
||||
vinRequestModule?: boolean
|
||||
priceRangeMarkups?: ProfilePriceRangeMarkupInput[]
|
||||
orderDiscounts?: ProfileOrderDiscountInput[]
|
||||
supplierMarkups?: ProfileSupplierMarkupInput[]
|
||||
brandMarkups?: ProfileBrandMarkupInput[]
|
||||
categoryMarkups?: ProfileCategoryMarkupInput[]
|
||||
excludedBrands?: string[]
|
||||
excludedCategories?: string[]
|
||||
paymentTypes?: ProfilePaymentTypeInput[]
|
||||
}
|
||||
|
||||
interface ProfilePriceRangeMarkupInput {
|
||||
priceFrom: number
|
||||
priceTo: number
|
||||
markupType: 'PERCENTAGE' | 'FIXED_AMOUNT'
|
||||
markupValue: number
|
||||
}
|
||||
|
||||
interface ProfileOrderDiscountInput {
|
||||
minOrderSum: number
|
||||
discountType: 'PERCENTAGE' | 'FIXED_AMOUNT'
|
||||
discountValue: number
|
||||
}
|
||||
|
||||
interface ProfileSupplierMarkupInput {
|
||||
supplierName: string
|
||||
markupType: 'PERCENTAGE' | 'FIXED_AMOUNT'
|
||||
markupValue: number
|
||||
}
|
||||
|
||||
interface ProfileBrandMarkupInput {
|
||||
brandName: string
|
||||
markupType: 'PERCENTAGE' | 'FIXED_AMOUNT'
|
||||
markupValue: number
|
||||
}
|
||||
|
||||
interface ProfileCategoryMarkupInput {
|
||||
categoryName: string
|
||||
markupType: 'PERCENTAGE' | 'FIXED_AMOUNT'
|
||||
markupValue: number
|
||||
}
|
||||
|
||||
interface ProfilePaymentTypeInput {
|
||||
paymentType: 'CASH' | 'CARD' | 'BANK_TRANSFER' | 'ONLINE' | 'CREDIT'
|
||||
isEnabled: boolean
|
||||
}
|
||||
|
||||
interface ClientVehicleInput {
|
||||
name: string
|
||||
vin?: string
|
||||
frame?: string
|
||||
licensePlate?: string
|
||||
brand?: string
|
||||
model?: string
|
||||
modification?: string
|
||||
year?: number
|
||||
mileage?: number
|
||||
comment?: string
|
||||
}
|
||||
|
||||
interface ClientDeliveryAddressInput {
|
||||
name: string
|
||||
address: string
|
||||
deliveryType: 'COURIER' | 'PICKUP' | 'POST' | 'TRANSPORT'
|
||||
comment?: string
|
||||
// Дополнительные поля для курьерской доставки
|
||||
entrance?: string
|
||||
floor?: string
|
||||
apartment?: string
|
||||
intercom?: string
|
||||
deliveryTime?: string
|
||||
contactPhone?: string
|
||||
}
|
||||
|
||||
interface ClientContactInput {
|
||||
phone?: string
|
||||
email?: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
interface ClientContractInput {
|
||||
contractNumber: string
|
||||
contractDate?: Date
|
||||
name: string
|
||||
ourLegalEntity?: string
|
||||
clientLegalEntity?: string
|
||||
balance?: number
|
||||
currency?: string
|
||||
isActive?: boolean
|
||||
isDefault?: boolean
|
||||
contractType?: string
|
||||
relationship?: string
|
||||
paymentDelay?: boolean
|
||||
creditLimit?: number
|
||||
delayDays?: number
|
||||
fileUrl?: string
|
||||
}
|
||||
|
||||
interface ClientLegalEntityInput {
|
||||
shortName: string
|
||||
fullName?: string
|
||||
form?: string
|
||||
legalAddress?: string
|
||||
actualAddress?: string
|
||||
taxSystem?: string
|
||||
responsiblePhone?: string
|
||||
responsiblePosition?: string
|
||||
responsibleName?: string
|
||||
accountant?: string
|
||||
signatory?: string
|
||||
registrationReasonCode?: string
|
||||
ogrn?: string
|
||||
inn: string
|
||||
vatPercent?: number
|
||||
}
|
||||
|
||||
interface ClientBankDetailsInput {
|
||||
name: string
|
||||
accountNumber: string
|
||||
bankName: string
|
||||
bik: string
|
||||
correspondentAccount: string
|
||||
}
|
||||
|
||||
interface ClientDiscountInput {
|
||||
name: string
|
||||
type: 'PERCENTAGE' | 'FIXED_AMOUNT'
|
||||
value: number
|
||||
isActive?: boolean
|
||||
validFrom?: Date
|
||||
validTo?: Date
|
||||
}
|
||||
|
||||
interface ClientStatusInput {
|
||||
name: string
|
||||
color?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface DiscountInput {
|
||||
name: string
|
||||
type: 'DISCOUNT' | 'PROMOCODE'
|
||||
code?: string
|
||||
minOrderAmount?: number
|
||||
discountType: 'PERCENTAGE' | 'FIXED_AMOUNT'
|
||||
discountValue: number
|
||||
isActive?: boolean
|
||||
validFrom?: Date
|
||||
validTo?: Date
|
||||
profileIds?: string[]
|
||||
}
|
||||
|
||||
interface ClientFilterInput {
|
||||
type?: 'INDIVIDUAL' | 'LEGAL_ENTITY'
|
||||
registeredFrom?: Date
|
||||
registeredTo?: Date
|
||||
unconfirmed?: boolean
|
||||
vehicleSearch?: string
|
||||
profileId?: string
|
||||
}
|
||||
|
||||
// Интерфейсы для заказов и платежей
|
||||
interface CreateOrderInput {
|
||||
clientId?: string
|
||||
clientEmail?: string
|
||||
clientPhone?: string
|
||||
clientName?: string
|
||||
items: OrderItemInput[]
|
||||
deliveryAddress?: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
interface OrderItemInput {
|
||||
productId?: string
|
||||
externalId?: string
|
||||
name: string
|
||||
article?: string
|
||||
brand?: string
|
||||
price: number
|
||||
quantity: number
|
||||
}
|
||||
|
||||
interface CreatePaymentInput {
|
||||
orderId: string
|
||||
returnUrl: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface FavoriteInput {
|
||||
productId?: string
|
||||
offerKey?: string
|
||||
name: string
|
||||
brand: string
|
||||
article: string
|
||||
price?: number
|
||||
currency?: string
|
||||
image?: string
|
||||
}
|
||||
|
||||
// Утилиты
|
||||
const createSlug = (text: string): string => {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[а-я]/g, (char) => {
|
||||
const map: { [key: string]: string } = {
|
||||
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
|
||||
'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
|
||||
'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
|
||||
'ф': 'f', 'х': 'h', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch',
|
||||
'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya'
|
||||
}
|
||||
return map[char] || char
|
||||
})
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
}
|
||||
|
||||
const getCategoryLevel = async (categoryId: string, level = 0): Promise<number> => {
|
||||
const category = await prisma.category.findUnique({
|
||||
where: { id: categoryId },
|
||||
select: { parentId: true }
|
||||
})
|
||||
|
||||
if (!category || !category.parentId) {
|
||||
return level
|
||||
}
|
||||
|
||||
return getCategoryLevel(category.parentId, level + 1)
|
||||
}
|
||||
|
||||
// Функция для расчета дней доставки из строки даты
|
||||
const calculateDeliveryDays = (deliveryDateStr: string): number => {
|
||||
if (!deliveryDateStr) return 0;
|
||||
|
||||
try {
|
||||
const deliveryDate = new Date(deliveryDateStr);
|
||||
const today = new Date();
|
||||
const diffTime = deliveryDate.getTime() - today.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return Math.max(0, diffDays);
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для получения контекста из глобальной переменной (больше не используется)
|
||||
function getContext(): Context {
|
||||
const context = (global as unknown as { __graphqlContext?: Context }).__graphqlContext || {}
|
||||
return context
|
||||
}
|
||||
|
||||
export const resolvers = {
|
||||
DateTime: {
|
||||
serialize: (date: Date | string) => {
|
||||
if (typeof date === 'string') {
|
||||
return date;
|
||||
}
|
||||
if (date instanceof Date) {
|
||||
return date.toISOString();
|
||||
}
|
||||
console.warn('DateTime serialize: неожиданный тип данных:', typeof date, date);
|
||||
return new Date(date).toISOString();
|
||||
},
|
||||
parseValue: (value: string) => new Date(value),
|
||||
parseLiteral: (ast: { value: string }) => new Date(ast.value),
|
||||
},
|
||||
|
||||
JSON: GraphQLJSON,
|
||||
|
||||
Category: {
|
||||
level: async (parent: { id: string }) => {
|
||||
return await getCategoryLevel(parent.id)
|
||||
},
|
||||
children: async (parent: { id: string }) => {
|
||||
return await prisma.category.findMany({
|
||||
where: { parentId: parent.id },
|
||||
orderBy: { name: 'asc' },
|
||||
include: {
|
||||
_count: { select: { products: true } }
|
||||
}
|
||||
})
|
||||
},
|
||||
products: async (parent: { id: string }) => {
|
||||
return await prisma.product.findMany({
|
||||
where: {
|
||||
categories: {
|
||||
some: { id: parent.id }
|
||||
}
|
||||
},
|
||||
include: {
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
categories: true
|
||||
},
|
||||
orderBy: { name: 'asc' }
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
Product: {
|
||||
categories: async (parent: { id: string }) => {
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id: parent.id },
|
||||
include: { categories: true }
|
||||
})
|
||||
return product?.categories || []
|
||||
},
|
||||
images: async (parent: { id: string }) => {
|
||||
return await prisma.productImage.findMany({
|
||||
where: { productId: parent.id },
|
||||
orderBy: { order: 'asc' }
|
||||
})
|
||||
},
|
||||
options: async (parent: { id: string }) => {
|
||||
return await prisma.productOption.findMany({
|
||||
where: { productId: parent.id },
|
||||
include: {
|
||||
option: { include: { values: true } },
|
||||
optionValue: true
|
||||
}
|
||||
})
|
||||
},
|
||||
characteristics: async (parent: { id: string }) => {
|
||||
return await prisma.productCharacteristic.findMany({
|
||||
where: { productId: parent.id },
|
||||
include: { characteristic: true }
|
||||
})
|
||||
},
|
||||
relatedProducts: async (parent: { id: string }) => {
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id: parent.id },
|
||||
include: { relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } } }
|
||||
})
|
||||
return product?.relatedProducts || []
|
||||
},
|
||||
accessoryProducts: async (parent: { id: string }) => {
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id: parent.id },
|
||||
include: { accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } } }
|
||||
})
|
||||
return product?.accessoryProducts || []
|
||||
}
|
||||
},
|
||||
|
||||
BalanceInvoice: {
|
||||
clientId: async (parent: { contract: { clientId: string } }) => {
|
||||
return parent.contract.clientId
|
||||
},
|
||||
expiresAt: (parent: { expiresAt: Date }) => {
|
||||
return parent.expiresAt.toISOString()
|
||||
},
|
||||
createdAt: (parent: { createdAt: Date }) => {
|
||||
return parent.createdAt.toISOString()
|
||||
},
|
||||
updatedAt: (parent: { updatedAt: Date }) => {
|
||||
return parent.updatedAt.toISOString()
|
||||
}
|
||||
},
|
||||
|
||||
Query: {
|
||||
users: async () => {
|
||||
try {
|
||||
return await prisma.user.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения пользователей:', error)
|
||||
throw new Error('Не удалось получить список пользователей')
|
||||
}
|
||||
},
|
||||
|
||||
user: async (_: unknown, { id }: { id: string }) => {
|
||||
try {
|
||||
return await prisma.user.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения пользователя:', error)
|
||||
throw new Error('Не удалось получить пользователя')
|
||||
}
|
||||
},
|
||||
|
||||
hasUsers: async () => {
|
||||
try {
|
||||
const count = await prisma.user.count()
|
||||
return count > 0
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки пользователей:', error)
|
||||
throw new Error('Не удалось проверить наличие пользователей')
|
||||
}
|
||||
},
|
||||
|
||||
me: async (_: unknown, __: unknown, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
return await prisma.user.findUnique({
|
||||
where: { id: context.userId }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения профиля:', error)
|
||||
throw new Error('Не удалось получить профиль пользователя')
|
||||
}
|
||||
},
|
||||
|
||||
// Счета на пополнение баланса
|
||||
balanceInvoices: async (_: unknown, __: unknown, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const invoices = await prisma.balanceInvoice.findMany({
|
||||
include: {
|
||||
contract: {
|
||||
include: {
|
||||
client: {
|
||||
include: {
|
||||
legalEntities: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
return invoices
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения счетов:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось получить список счетов')
|
||||
}
|
||||
},
|
||||
|
||||
auditLogs: async (_: unknown, { limit = 50, offset = 0 }: { limit?: number; offset?: number }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId || context.userRole !== 'ADMIN') {
|
||||
throw new Error('Недостаточно прав для просмотра логов аудита')
|
||||
}
|
||||
|
||||
return await prisma.auditLog.findMany({
|
||||
include: {
|
||||
user: true
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
skip: offset
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения логов аудита:', error)
|
||||
throw new Error('Не удалось получить логи аудита')
|
||||
}
|
||||
},
|
||||
|
||||
auditLogsCount: async (_: unknown, __: unknown, context: Context) => {
|
||||
try {
|
||||
if (!context.userId || context.userRole !== 'ADMIN') {
|
||||
throw new Error('Недостаточно прав для просмотра логов аудита')
|
||||
}
|
||||
|
||||
return await prisma.auditLog.count()
|
||||
} catch (error) {
|
||||
console.error('Ошибка подсчета логов аудита:', error)
|
||||
throw new Error('Не удалось подсчитать логи аудита')
|
||||
}
|
||||
},
|
||||
|
||||
// Каталог
|
||||
categories: async () => {
|
||||
try {
|
||||
return await prisma.category.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
include: {
|
||||
children: true,
|
||||
_count: { select: { products: true } }
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения категорий:', error)
|
||||
throw new Error('Не удалось получить категории')
|
||||
}
|
||||
},
|
||||
|
||||
category: async (_: unknown, { id }: { id: string }) => {
|
||||
try {
|
||||
return await prisma.category.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
parent: true,
|
||||
children: true,
|
||||
products: {
|
||||
include: {
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
categories: true
|
||||
}
|
||||
},
|
||||
_count: { select: { products: true } }
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения категории:', error)
|
||||
throw new Error('Не удалось получить категорию')
|
||||
}
|
||||
},
|
||||
|
||||
categoryBySlug: async (_: unknown, { slug }: { slug: string }) => {
|
||||
try {
|
||||
return await prisma.category.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
parent: true,
|
||||
children: true,
|
||||
products: {
|
||||
include: {
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
categories: true
|
||||
}
|
||||
},
|
||||
_count: { select: { products: true } }
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения категории по slug:', error)
|
||||
throw new Error('Не удалось получить категорию')
|
||||
}
|
||||
},
|
||||
|
||||
products: async (_: unknown, { categoryId, search, limit = 50, offset = 0 }: {
|
||||
categoryId?: string; search?: string; limit?: number; offset?: number
|
||||
}) => {
|
||||
try {
|
||||
const where: Record<string, unknown> = {}
|
||||
|
||||
if (categoryId) {
|
||||
where.categories = { some: { id: categoryId } }
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ article: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
|
||||
return await prisma.product.findMany({
|
||||
where,
|
||||
include: {
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
categories: true,
|
||||
characteristics: { include: { characteristic: true } }
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
take: limit,
|
||||
skip: offset
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения товаров:', error)
|
||||
throw new Error('Не удалось получить товары')
|
||||
}
|
||||
},
|
||||
|
||||
productsCount: async (_: unknown, { categoryId, search }: {
|
||||
categoryId?: string; search?: string
|
||||
}) => {
|
||||
try {
|
||||
const where: Record<string, unknown> = {}
|
||||
|
||||
if (categoryId) {
|
||||
where.categories = { some: { id: categoryId } }
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ article: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
|
||||
return await prisma.product.count({ where })
|
||||
} catch (error) {
|
||||
console.error('Ошибка подсчета товаров:', error)
|
||||
throw new Error('Не удалось подсчитать товары')
|
||||
}
|
||||
},
|
||||
|
||||
product: async (_: unknown, { id }: { id: string }) => {
|
||||
try {
|
||||
return await prisma.product.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
categories: true,
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
options: {
|
||||
include: {
|
||||
option: { include: { values: true } },
|
||||
optionValue: true
|
||||
}
|
||||
},
|
||||
characteristics: { include: { characteristic: true } },
|
||||
relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } },
|
||||
accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } }
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения товара:', error)
|
||||
throw new Error('Не удалось получить товар')
|
||||
}
|
||||
},
|
||||
|
||||
productBySlug: async (_: unknown, { slug }: { slug: string }) => {
|
||||
try {
|
||||
return await prisma.product.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
categories: true,
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
options: {
|
||||
include: {
|
||||
option: { include: { values: true } },
|
||||
optionValue: true
|
||||
}
|
||||
},
|
||||
characteristics: { include: { characteristic: true } },
|
||||
relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } },
|
||||
accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } }
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения товара по slug:', error)
|
||||
throw new Error('Не удалось получить товар')
|
||||
}
|
||||
},
|
||||
|
||||
productHistory: async (_: unknown, { productId }: { productId: string }) => {
|
||||
try {
|
||||
return await prisma.productHistory.findMany({
|
||||
where: { productId },
|
||||
include: { user: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения истории товара:', error)
|
||||
throw new Error('Не удалось получить историю товара')
|
||||
}
|
||||
},
|
||||
|
||||
options: async () => {
|
||||
try {
|
||||
return await prisma.option.findMany({
|
||||
include: { values: true },
|
||||
orderBy: { name: 'asc' }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения опций:', error)
|
||||
throw new Error('Не удалось получить опции')
|
||||
}
|
||||
},
|
||||
|
||||
characteristics: async () => {
|
||||
try {
|
||||
return await prisma.characteristic.findMany({
|
||||
orderBy: { name: 'asc' }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения характеристик:', error)
|
||||
throw new Error('Не удалось получить характеристики')
|
||||
}
|
||||
},
|
||||
|
||||
// Клиенты
|
||||
clients: async (_: unknown, {
|
||||
filter, search, limit = 50, offset = 0, sortBy = 'createdAt', sortOrder = 'desc'
|
||||
}: {
|
||||
filter?: ClientFilterInput; search?: string; limit?: number; offset?: number;
|
||||
sortBy?: string; sortOrder?: string
|
||||
}) => {
|
||||
try {
|
||||
const where: Record<string, unknown> = {}
|
||||
|
||||
if (filter) {
|
||||
if (filter.type) {
|
||||
where.type = filter.type
|
||||
}
|
||||
if (filter.registeredFrom || filter.registeredTo) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
where.createdAt = {} as any
|
||||
if (filter.registeredFrom) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(where.createdAt as any).gte = filter.registeredFrom
|
||||
}
|
||||
if (filter.registeredTo) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(where.createdAt as any).lte = filter.registeredTo
|
||||
}
|
||||
}
|
||||
if (filter.unconfirmed) {
|
||||
where.isConfirmed = false
|
||||
}
|
||||
if (filter.profileId) {
|
||||
where.profileId = filter.profileId
|
||||
}
|
||||
if (filter.vehicleSearch) {
|
||||
where.vehicles = {
|
||||
some: {
|
||||
OR: [
|
||||
{ vin: { contains: filter.vehicleSearch, mode: 'insensitive' } },
|
||||
{ frame: { contains: filter.vehicleSearch, mode: 'insensitive' } },
|
||||
{ licensePlate: { contains: filter.vehicleSearch, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ email: { contains: search, mode: 'insensitive' } },
|
||||
{ phone: { contains: search, mode: 'insensitive' } },
|
||||
{ clientNumber: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
|
||||
const orderBy: Record<string, string> = {}
|
||||
orderBy[sortBy] = sortOrder
|
||||
|
||||
return await prisma.client.findMany({
|
||||
where,
|
||||
include: {
|
||||
profile: true,
|
||||
vehicles: true,
|
||||
discounts: true
|
||||
},
|
||||
orderBy,
|
||||
take: limit,
|
||||
skip: offset
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения клиентов:', error)
|
||||
throw new Error('Не удалось получить клиентов')
|
||||
}
|
||||
},
|
||||
|
||||
client: async (_: unknown, { id }: { id: string }) => {
|
||||
try {
|
||||
return await prisma.client.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
profile: true,
|
||||
manager: true,
|
||||
vehicles: true,
|
||||
discounts: true,
|
||||
deliveryAddresses: true,
|
||||
contacts: true,
|
||||
contracts: true,
|
||||
legalEntities: {
|
||||
include: {
|
||||
bankDetails: true
|
||||
}
|
||||
},
|
||||
bankDetails: {
|
||||
include: {
|
||||
legalEntity: true
|
||||
}
|
||||
},
|
||||
balanceHistory: {
|
||||
include: {
|
||||
user: true
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения клиента:', error)
|
||||
throw new Error('Не удалось получить клиента')
|
||||
}
|
||||
},
|
||||
|
||||
clientsCount: async (_: unknown, { filter, search }: { filter?: ClientFilterInput; search?: string }) => {
|
||||
try {
|
||||
const where: Record<string, unknown> = {}
|
||||
|
||||
if (filter) {
|
||||
if (filter.type) {
|
||||
where.type = filter.type
|
||||
}
|
||||
if (filter.registeredFrom || filter.registeredTo) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
where.createdAt = {} as any
|
||||
if (filter.registeredFrom) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(where.createdAt as any).gte = filter.registeredFrom
|
||||
}
|
||||
if (filter.registeredTo) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(where.createdAt as any).lte = filter.registeredTo
|
||||
}
|
||||
}
|
||||
if (filter.unconfirmed) {
|
||||
where.isConfirmed = false
|
||||
}
|
||||
if (filter.profileId) {
|
||||
where.profileId = filter.profileId
|
||||
}
|
||||
if (filter.vehicleSearch) {
|
||||
where.vehicles = {
|
||||
some: {
|
||||
OR: [
|
||||
{ vin: { contains: filter.vehicleSearch, mode: 'insensitive' } },
|
||||
{ frame: { contains: filter.vehicleSearch, mode: 'insensitive' } },
|
||||
{ licensePlate: { contains: filter.vehicleSearch, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ email: { contains: search, mode: 'insensitive' } },
|
||||
{ phone: { contains: search, mode: 'insensitive' } },
|
||||
{ clientNumber: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
|
||||
return await prisma.client.count({ where })
|
||||
} catch (error) {
|
||||
console.error('Ошибка подсчета клиентов:', error)
|
||||
throw new Error('Не удалось подсчитать клиентов')
|
||||
}
|
||||
},
|
||||
|
||||
// Запросы для гаража клиентов
|
||||
userVehicles: async () => {
|
||||
try {
|
||||
const context = getContext()
|
||||
if (!context.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Проверяем существует ли клиент, если нет - создаем только для временных клиентов
|
||||
let client = await prisma.client.findUnique({
|
||||
where: { id: context.clientId }
|
||||
})
|
||||
|
||||
if (!client) {
|
||||
if (context.clientId.startsWith('client_') && context.clientId.length > 30) {
|
||||
client = await prisma.client.create({
|
||||
data: {
|
||||
id: context.clientId,
|
||||
clientNumber: `CLIENT_${Date.now()}`,
|
||||
type: 'INDIVIDUAL',
|
||||
name: 'Гость',
|
||||
phone: '+7',
|
||||
isConfirmed: false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error('Клиент не найден в системе')
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.clientVehicle.findMany({
|
||||
where: { clientId: context.clientId },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения автомобилей:', error)
|
||||
throw new Error('Не удалось получить автомобили')
|
||||
}
|
||||
},
|
||||
|
||||
// Получение данных авторизованного клиента
|
||||
clientMe: async () => {
|
||||
try {
|
||||
const context = getContext()
|
||||
console.log('clientMe резолвер: контекст:', context)
|
||||
if (!context.clientId) {
|
||||
console.log('clientMe резолвер: clientId отсутствует')
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
console.log('clientMe резолвер: получаем данные для clientId:', context.clientId)
|
||||
const client = await prisma.client.findUnique({
|
||||
where: { id: context.clientId },
|
||||
include: {
|
||||
legalEntities: {
|
||||
include: {
|
||||
bankDetails: {
|
||||
include: {
|
||||
legalEntity: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
profile: true,
|
||||
vehicles: true,
|
||||
deliveryAddresses: true,
|
||||
contacts: true,
|
||||
contracts: true,
|
||||
bankDetails: {
|
||||
include: {
|
||||
legalEntity: true
|
||||
}
|
||||
},
|
||||
discounts: true
|
||||
}
|
||||
})
|
||||
console.log('clientMe резолвер: найден клиент:', client ? client.id : 'null')
|
||||
|
||||
// Принудительно заменяем null bankDetails на пустые массивы
|
||||
if (client && client.legalEntities) {
|
||||
client.legalEntities = client.legalEntities.map(entity => ({
|
||||
...entity,
|
||||
bankDetails: entity.bankDetails || []
|
||||
}))
|
||||
}
|
||||
|
||||
return client
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения данных клиента:', error)
|
||||
throw new Error('Не удалось получить данные клиента')
|
||||
}
|
||||
},
|
||||
|
||||
// Получение избранного авторизованного клиента
|
||||
favorites: async (_: unknown, _args: unknown, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Удаляем префикс client_ если он есть
|
||||
const cleanClientId = actualContext.clientId.startsWith('client_')
|
||||
? actualContext.clientId.substring(7)
|
||||
: actualContext.clientId
|
||||
|
||||
const favorites = await prisma.favorite.findMany({
|
||||
where: {
|
||||
clientId: cleanClientId
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
include: {
|
||||
client: true
|
||||
}
|
||||
})
|
||||
|
||||
return favorites
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения избранного:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось получить избранное')
|
||||
}
|
||||
},
|
||||
|
||||
vehicleSearchHistory: async (_: unknown, args: unknown, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
// Для неавторизованных пользователей возвращаем пустую историю
|
||||
return []
|
||||
}
|
||||
|
||||
// Определяем clientId, убирая префикс client_ если он есть
|
||||
const clientIdParts = actualContext.clientId.split('_')
|
||||
let clientId = actualContext.clientId
|
||||
|
||||
if (clientIdParts.length >= 3) {
|
||||
clientId = clientIdParts[1] // client_ID_timestamp -> ID
|
||||
} else if (clientIdParts.length === 2) {
|
||||
clientId = clientIdParts[1] // client_ID -> ID
|
||||
}
|
||||
|
||||
console.log('vehicleSearchHistory: получение VIN истории для клиента:', clientId)
|
||||
|
||||
// Проверяем существует ли клиент
|
||||
const client = await prisma.client.findUnique({
|
||||
where: { id: clientId }
|
||||
})
|
||||
|
||||
if (!client) {
|
||||
console.log('vehicleSearchHistory: клиент не найден:', clientId)
|
||||
return []
|
||||
}
|
||||
|
||||
// Получаем записи истории только с типом VIN
|
||||
const vinHistoryItems = await prisma.partsSearchHistory.findMany({
|
||||
where: {
|
||||
clientId,
|
||||
searchType: 'VIN' // Фильтруем только VIN запросы
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20 // Ограничиваем количество записей
|
||||
})
|
||||
|
||||
console.log(`vehicleSearchHistory: найдено ${vinHistoryItems.length} VIN записей`)
|
||||
|
||||
// Преобразуем данные в формат VehicleSearchHistory
|
||||
const historyItems = vinHistoryItems.map(item => ({
|
||||
id: item.id,
|
||||
vin: item.searchQuery, // VIN записан в searchQuery
|
||||
brand: item.vehicleBrand || item.brand,
|
||||
model: item.vehicleModel,
|
||||
searchDate: item.createdAt instanceof Date ? item.createdAt.toISOString() : item.createdAt,
|
||||
searchQuery: item.searchQuery
|
||||
}))
|
||||
|
||||
return historyItems
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения истории VIN поиска:', error)
|
||||
throw new Error('Не удалось получить историю поиска')
|
||||
}
|
||||
},
|
||||
|
||||
// История поиска запчастей
|
||||
partsSearchHistory: async (_: unknown, { limit = 50, offset = 0 }: { limit?: number; offset?: number }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
// Для неавторизованных пользователей возвращаем пустую историю
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false
|
||||
}
|
||||
}
|
||||
|
||||
// Определяем clientId, убирая префикс client_ если он есть
|
||||
const clientIdParts = actualContext.clientId.split('_')
|
||||
let clientId = actualContext.clientId
|
||||
|
||||
if (clientIdParts.length >= 3) {
|
||||
clientId = clientIdParts[1] // client_ID_timestamp -> ID
|
||||
} else if (clientIdParts.length === 2) {
|
||||
clientId = clientIdParts[1] // client_ID -> ID
|
||||
}
|
||||
|
||||
console.log('partsSearchHistory: получение истории для клиента:', clientId)
|
||||
console.log('prisma.partsSearchHistory:', typeof prisma.partsSearchHistory)
|
||||
|
||||
// Проверяем существует ли клиент
|
||||
const client = await prisma.client.findUnique({
|
||||
where: { id: clientId }
|
||||
})
|
||||
|
||||
if (!client) {
|
||||
console.log('partsSearchHistory: клиент не найден:', clientId)
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
hasMore: false
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, что Prisma Client правильно инициализирован
|
||||
if (!prisma.partsSearchHistory) {
|
||||
console.error('prisma.partsSearchHistory не определен')
|
||||
throw new Error('Ошибка инициализации базы данных')
|
||||
}
|
||||
|
||||
// Получаем общее количество записей
|
||||
const total = await prisma.partsSearchHistory.count({
|
||||
where: { clientId }
|
||||
})
|
||||
|
||||
// Получаем записи истории
|
||||
const historyItems = await prisma.partsSearchHistory.findMany({
|
||||
where: { clientId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
skip: offset
|
||||
})
|
||||
|
||||
console.log(`partsSearchHistory: найдено ${historyItems.length} записей`)
|
||||
|
||||
const items = historyItems.map(item => ({
|
||||
id: item.id,
|
||||
searchQuery: item.searchQuery,
|
||||
searchType: item.searchType,
|
||||
brand: item.brand,
|
||||
articleNumber: item.articleNumber,
|
||||
vehicleInfo: item.vehicleBrand || item.vehicleModel || item.vehicleYear ? {
|
||||
brand: item.vehicleBrand,
|
||||
model: item.vehicleModel,
|
||||
year: item.vehicleYear
|
||||
} : null,
|
||||
resultCount: item.resultCount,
|
||||
createdAt: item.createdAt instanceof Date ? item.createdAt.toISOString() : item.createdAt
|
||||
}))
|
||||
|
||||
return {
|
||||
items,
|
||||
total,
|
||||
hasMore: offset + limit < total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения истории поиска запчастей:', error)
|
||||
throw new Error('Не удалось получить историю поиска запчастей')
|
||||
}
|
||||
},
|
||||
|
||||
searchVehicleByVin: async (_: unknown, { vin }: { vin: string }) => {
|
||||
try {
|
||||
// Временная заглушка - возвращаем объект с переданным VIN
|
||||
// В будущем здесь будет реальная логика поиска по VIN
|
||||
return {
|
||||
vin,
|
||||
brand: null,
|
||||
model: null,
|
||||
modification: null,
|
||||
year: null,
|
||||
bodyType: null,
|
||||
engine: null,
|
||||
transmission: null,
|
||||
drive: null,
|
||||
fuel: null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска по VIN:', error)
|
||||
throw new Error('Не удалось найти автомобиль по VIN')
|
||||
}
|
||||
},
|
||||
|
||||
clientProfiles: async () => {
|
||||
try {
|
||||
return await prisma.clientProfile.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
include: {
|
||||
priceRangeMarkups: true,
|
||||
orderDiscounts: true,
|
||||
supplierMarkups: true,
|
||||
brandMarkups: true,
|
||||
categoryMarkups: true,
|
||||
excludedBrands: true,
|
||||
excludedCategories: true,
|
||||
paymentTypes: true,
|
||||
_count: { select: { clients: true } }
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения профилей клиентов:', error)
|
||||
throw new Error('Не удалось получить профили клиентов')
|
||||
}
|
||||
},
|
||||
|
||||
clientProfile: async (_: unknown, { id }: { id: string }) => {
|
||||
try {
|
||||
return await prisma.clientProfile.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
clients: true,
|
||||
priceRangeMarkups: true,
|
||||
orderDiscounts: true,
|
||||
supplierMarkups: true,
|
||||
brandMarkups: true,
|
||||
categoryMarkups: true,
|
||||
excludedBrands: true,
|
||||
excludedCategories: true,
|
||||
paymentTypes: true,
|
||||
_count: { select: { clients: true } }
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения профиля клиента:', error)
|
||||
throw new Error('Не удалось получить профиль клиента')
|
||||
}
|
||||
},
|
||||
|
||||
clientStatuses: async () => {
|
||||
try {
|
||||
return await prisma.clientStatus.findMany({
|
||||
orderBy: { name: 'asc' }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения статусов клиентов:', error)
|
||||
throw new Error('Не удалось получить статусы клиентов')
|
||||
}
|
||||
},
|
||||
|
||||
clientStatus: async (_: unknown, { id }: { id: string }) => {
|
||||
try {
|
||||
return await prisma.clientStatus.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения статуса клиента:', error)
|
||||
throw new Error('Не удалось получить статус клиента')
|
||||
}
|
||||
},
|
||||
|
||||
// Скидки и промокоды
|
||||
discounts: async () => {
|
||||
try {
|
||||
return await prisma.discount.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
include: {
|
||||
profiles: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения скидок:', error)
|
||||
throw new Error('Не удалось получить скидки')
|
||||
}
|
||||
},
|
||||
|
||||
discount: async (_: unknown, { id }: { id: string }) => {
|
||||
try {
|
||||
return await prisma.discount.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
profiles: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения скидки:', error)
|
||||
throw new Error('Не удалось получить скидку')
|
||||
}
|
||||
},
|
||||
|
||||
// Laximo интеграция
|
||||
laximoBrands: async () => {
|
||||
return await laximoService.getListCatalogs()
|
||||
},
|
||||
|
||||
laximoCatalogInfo: async (_: unknown, { catalogCode }: { catalogCode: string }) => {
|
||||
try {
|
||||
console.log('🔍 Запрос информации о каталоге:', catalogCode)
|
||||
const result = await laximoService.getCatalogInfo(catalogCode)
|
||||
console.log('📋 Результат getCatalogInfo:', result ? 'найден' : 'не найден')
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка получения информации о каталоге:', error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
laximoWizard2: async (_: unknown, { catalogCode, ssd }: { catalogCode: string; ssd?: string }) => {
|
||||
try {
|
||||
return await laximoService.getWizard2(catalogCode, ssd || '')
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения параметров wizard:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoFindVehicle: async (_: unknown, { catalogCode, vin }: { catalogCode: string; vin: string }, context: Context) => {
|
||||
try {
|
||||
// Если catalogCode пустой, делаем глобальный поиск
|
||||
if (!catalogCode || catalogCode.trim() === '') {
|
||||
console.log('🌍 Глобальный поиск автомобиля по VIN/Frame:', vin)
|
||||
const result = await laximoService.findVehicleGlobal(vin)
|
||||
|
||||
// Сохраняем в историю поиска с информацией о первом найденном автомобиле
|
||||
let vehicleInfo: { brand?: string; model?: string; year?: number } | undefined = undefined
|
||||
if (result && result.length > 0) {
|
||||
const firstVehicle = result[0]
|
||||
vehicleInfo = {
|
||||
brand: firstVehicle.brand,
|
||||
model: firstVehicle.model,
|
||||
year: firstVehicle.year ? parseInt(firstVehicle.year, 10) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
await saveSearchHistory(
|
||||
context,
|
||||
vin,
|
||||
'VIN',
|
||||
undefined,
|
||||
undefined,
|
||||
vehicleInfo,
|
||||
result.length
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const result = await laximoService.findVehicle(catalogCode, vin)
|
||||
|
||||
// Сохраняем в историю поиска с информацией о первом найденном автомобиле
|
||||
let vehicleInfo: { brand?: string; model?: string; year?: number } | undefined = undefined
|
||||
if (result && result.length > 0) {
|
||||
const firstVehicle = result[0]
|
||||
vehicleInfo = {
|
||||
brand: firstVehicle.brand,
|
||||
model: firstVehicle.model,
|
||||
year: firstVehicle.year ? parseInt(firstVehicle.year, 10) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
await saveSearchHistory(
|
||||
context,
|
||||
vin,
|
||||
'VIN',
|
||||
catalogCode,
|
||||
undefined,
|
||||
vehicleInfo,
|
||||
result.length
|
||||
)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска автомобиля по VIN:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoFindVehicleByWizard: async (_: unknown, { catalogCode, ssd }: { catalogCode: string; ssd: string }, context: Context) => {
|
||||
try {
|
||||
const result = await laximoService.findVehicleByWizard(catalogCode, ssd)
|
||||
|
||||
// Сохраняем в историю поиска
|
||||
await saveSearchHistory(
|
||||
context,
|
||||
`Поиск по параметрам в ${catalogCode}`,
|
||||
'WIZARD',
|
||||
catalogCode,
|
||||
undefined,
|
||||
undefined,
|
||||
result.length
|
||||
)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска автомобиля по wizard:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoFindVehicleByPlate: async (_: unknown, { catalogCode, plateNumber }: { catalogCode: string; plateNumber: string }, context: Context) => {
|
||||
try {
|
||||
const result = await laximoService.findVehicleByPlateNumber(catalogCode, plateNumber)
|
||||
|
||||
// Сохраняем в историю поиска с информацией о первом найденном автомобиле
|
||||
let vehicleInfo: { brand?: string; model?: string; year?: number } | undefined = undefined
|
||||
if (result && result.length > 0) {
|
||||
const firstVehicle = result[0]
|
||||
vehicleInfo = {
|
||||
brand: firstVehicle.brand,
|
||||
model: firstVehicle.model,
|
||||
year: firstVehicle.year ? parseInt(firstVehicle.year, 10) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
await saveSearchHistory(
|
||||
context,
|
||||
plateNumber,
|
||||
'PLATE',
|
||||
catalogCode,
|
||||
undefined,
|
||||
vehicleInfo,
|
||||
result.length
|
||||
)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска автомобиля по госномеру:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoFindVehicleByPlateGlobal: async (_: unknown, { plateNumber }: { plateNumber: string }, context: Context) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - Глобальный поиск автомобиля по госномеру:', plateNumber)
|
||||
const result = await laximoService.findVehicleByPlateNumberGlobal(plateNumber)
|
||||
console.log('📋 Результат глобального поиска по госномеру:', result ? `найдено ${result.length} автомобилей` : 'результат пустой')
|
||||
|
||||
// Сохраняем в историю поиска с информацией о первом найденном автомобиле
|
||||
let vehicleInfo: { brand?: string; model?: string; year?: number } | undefined = undefined
|
||||
if (result && result.length > 0) {
|
||||
const firstVehicle = result[0]
|
||||
vehicleInfo = {
|
||||
brand: firstVehicle.brand,
|
||||
model: firstVehicle.model,
|
||||
year: firstVehicle.year ? parseInt(firstVehicle.year, 10) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
await saveSearchHistory(
|
||||
context,
|
||||
plateNumber,
|
||||
'PLATE',
|
||||
undefined,
|
||||
undefined,
|
||||
vehicleInfo,
|
||||
result.length
|
||||
)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка глобального поиска автомобиля по госномеру:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoFindPartReferences: async (_: unknown, { partNumber }: { partNumber: string }) => {
|
||||
try {
|
||||
return await laximoService.findPartReferences(partNumber)
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска каталогов по артикулу:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoFindApplicableVehicles: async (_: unknown, { catalogCode, partNumber }: { catalogCode: string; partNumber: string }, context: Context) => {
|
||||
try {
|
||||
const result = await laximoService.findApplicableVehicles(catalogCode, partNumber)
|
||||
|
||||
// Сохраняем в историю поиска
|
||||
await saveSearchHistory(
|
||||
context,
|
||||
partNumber,
|
||||
'PART_VEHICLES',
|
||||
catalogCode,
|
||||
partNumber,
|
||||
undefined,
|
||||
result.length
|
||||
)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска автомобилей по артикулу:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoFindVehiclesByPartNumber: async (_: unknown, { partNumber }: { partNumber: string }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - Комплексный поиск автомобилей по артикулу:', partNumber)
|
||||
const result = await laximoService.findVehiclesByPartNumber(partNumber)
|
||||
console.log('📋 Результат комплексного поиска:', result ? `найдено ${result.totalVehicles} автомобилей в ${result.catalogs.length} каталогах` : 'результат null')
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка комплексного поиска автомобилей по артикулу:', error)
|
||||
return {
|
||||
partNumber,
|
||||
catalogs: [],
|
||||
totalVehicles: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
laximoVehicleInfo: async (_: unknown, { catalogCode, vehicleId, ssd, localized }: { catalogCode: string; vehicleId: string; ssd?: string; localized: boolean }) => {
|
||||
try {
|
||||
return await laximoService.getVehicleInfo(catalogCode, vehicleId, ssd, localized)
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения информации об автомобиле:', error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
laximoQuickGroups: async (_: unknown, { catalogCode, vehicleId, ssd }: { catalogCode: string; vehicleId: string; ssd?: string }) => {
|
||||
try {
|
||||
console.log('🔧 GraphQL Resolver - получение групп быстрого поиска:', { catalogCode, vehicleId, ssd: ssd?.substring(0, 30) })
|
||||
|
||||
let groups: any[] = []
|
||||
|
||||
// Сначала пробуем стандартный метод getListQuickGroup
|
||||
try {
|
||||
groups = await laximoService.getListQuickGroup(catalogCode, vehicleId, ssd)
|
||||
console.log('✅ Получено групп через getListQuickGroup:', groups.length)
|
||||
} catch (quickGroupError) {
|
||||
console.warn('⚠️ Ошибка getListQuickGroup:', quickGroupError)
|
||||
|
||||
// Альтернативный метод - используем ListCategories
|
||||
try {
|
||||
console.log('🔄 Пробуем альтернативный метод - ListCategories')
|
||||
groups = await laximoService.getListCategories(catalogCode, vehicleId, ssd)
|
||||
console.log('✅ Получено категорий через getListCategories:', groups.length)
|
||||
} catch (categoriesError) {
|
||||
console.warn('⚠️ Ошибка getListCategories:', categoriesError)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎯 GraphQL Resolver - итоговый результат:')
|
||||
console.log('📊 Общее количество групп:', groups.length)
|
||||
|
||||
if (groups.length > 0) {
|
||||
console.log('📋 Первые 5 групп:')
|
||||
groups.slice(0, 5).forEach((group, index) => {
|
||||
console.log(` ${index + 1}. ${group.name} (ID: ${group.quickgroupid}, link: ${group.link})`)
|
||||
})
|
||||
}
|
||||
|
||||
// Подсчитываем детали в подгруппах
|
||||
groups.forEach((group, index) => {
|
||||
const countChildren = (g: any): number => {
|
||||
let count = 1
|
||||
if (g.children && g.children.length > 0) {
|
||||
g.children.forEach((child: any) => {
|
||||
count += countChildren(child)
|
||||
})
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
const totalChildren = countChildren(group) - 1 // Исключаем саму группу
|
||||
console.log(`📂 Группа ${index + 1}: ${group.name} - всего подэлементов: ${totalChildren}`)
|
||||
|
||||
if (group.children && group.children.length > 0) {
|
||||
group.children.forEach((child, childIndex) => {
|
||||
console.log(` └─ Дочерняя группа ${childIndex + 1}:`, {
|
||||
quickgroupid: child.quickgroupid,
|
||||
name: child.name,
|
||||
link: child.link,
|
||||
code: child.code || 'отсутствует',
|
||||
children: child.children?.length || 0
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return groups
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка получения групп быстрого поиска:', error)
|
||||
console.error('❌ Stack trace:', error instanceof Error ? error.stack : 'нет stack trace')
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoQuickGroupsWithXML: async (_: unknown, { catalogCode, vehicleId, ssd }: { catalogCode: string; vehicleId: string; ssd?: string }) => {
|
||||
try {
|
||||
console.log('🔧 GraphQL Resolver - получение групп быстрого поиска с RAW XML:', { catalogCode, vehicleId, ssd: ssd?.substring(0, 30) })
|
||||
|
||||
const result = await laximoService.getListQuickGroupWithXML(catalogCode, vehicleId, ssd)
|
||||
|
||||
console.log('🎯 GraphQL Resolver - результат от LaximoService:')
|
||||
console.log('📊 Общее количество групп:', result.groups.length)
|
||||
console.log('📄 RAW XML длина:', result.rawXML.length)
|
||||
|
||||
return {
|
||||
groups: result.groups,
|
||||
rawXML: result.rawXML
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка получения групп быстрого поиска с XML:', error)
|
||||
return {
|
||||
groups: [],
|
||||
rawXML: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
laximoCategories: async (_: unknown, { catalogCode, vehicleId, ssd }: { catalogCode: string; vehicleId?: string; ssd?: string }) => {
|
||||
try {
|
||||
console.log('🔍 Запрос категорий каталога:', catalogCode, 'vehicleId:', vehicleId, 'ssd:', ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует')
|
||||
return await laximoService.getListCategories(catalogCode, vehicleId, ssd)
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения категорий каталога:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoUnits: async (_: unknown, { catalogCode, vehicleId, ssd, categoryId }: { catalogCode: string; vehicleId?: string; ssd?: string; categoryId?: string }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - запрос узлов каталога:', {
|
||||
catalogCode,
|
||||
vehicleId,
|
||||
categoryId,
|
||||
hasSSD: !!ssd,
|
||||
ssdLength: ssd?.length
|
||||
})
|
||||
|
||||
const result = await laximoService.getListUnits(catalogCode, vehicleId, ssd, categoryId)
|
||||
console.log('✅ GraphQL Resolver - получено узлов каталога:', result?.length || 0)
|
||||
|
||||
if (result && result.length > 0) {
|
||||
console.log('📦 Первый узел:', {
|
||||
quickgroupid: result[0].quickgroupid,
|
||||
name: result[0].name,
|
||||
code: result[0].code,
|
||||
hasImageUrl: !!result[0].imageurl,
|
||||
imageUrl: result[0].imageurl ? result[0].imageurl.substring(0, 80) + '...' : 'отсутствует'
|
||||
})
|
||||
}
|
||||
|
||||
return result || []
|
||||
} catch (error) {
|
||||
console.error('❌ GraphQL Resolver - ошибка получения узлов каталога:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoQuickDetail: async (_: unknown, { catalogCode, vehicleId, quickGroupId, ssd }: { catalogCode: string; vehicleId: string; quickGroupId: string; ssd: string }) => {
|
||||
try {
|
||||
console.log('🔍 Запрос деталей группы быстрого поиска:', {
|
||||
catalogCode,
|
||||
vehicleId,
|
||||
quickGroupId,
|
||||
quickGroupIdType: typeof quickGroupId,
|
||||
quickGroupIdLength: quickGroupId?.length,
|
||||
ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует'
|
||||
})
|
||||
|
||||
// Валидация параметров
|
||||
if (!quickGroupId || quickGroupId.trim() === '') {
|
||||
console.error('❌ Пустой quickGroupId:', quickGroupId)
|
||||
throw new Error(`Пустой ID группы: "${quickGroupId}"`)
|
||||
}
|
||||
|
||||
return await laximoService.getListQuickDetail(catalogCode, vehicleId, quickGroupId, ssd)
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения деталей группы быстрого поиска:', error)
|
||||
throw error // Пробрасываем ошибку наверх
|
||||
}
|
||||
},
|
||||
|
||||
laximoOEMSearch: async (_: unknown, { catalogCode, vehicleId, oemNumber, ssd }: { catalogCode: string; vehicleId: string; oemNumber: string; ssd: string }) => {
|
||||
try {
|
||||
console.log('🔍 Поиск детали по OEM номеру:', { catalogCode, vehicleId, oemNumber })
|
||||
return await laximoService.getOEMPartApplicability(catalogCode, vehicleId, oemNumber, ssd)
|
||||
} catch (err) {
|
||||
console.error('Ошибка поиска детали по OEM номеру:', err)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
laximoFulltextSearch: async (_: unknown, { catalogCode, vehicleId, searchQuery, ssd }: { catalogCode: string; vehicleId: string; searchQuery: string; ssd: string }, context: Context) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - Поиск деталей по названию:', { catalogCode, vehicleId, searchQuery, ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует' })
|
||||
|
||||
// Сначала проверим поддержку полнотекстового поиска каталогом
|
||||
const catalogInfo = await laximoService.getCatalogInfo(catalogCode)
|
||||
if (catalogInfo) {
|
||||
const hasFulltextSearch = catalogInfo.features.some(f => f.name === 'fulltextsearch')
|
||||
console.log(`📋 Каталог ${catalogCode} поддерживает полнотекстовый поиск:`, hasFulltextSearch)
|
||||
|
||||
if (!hasFulltextSearch) {
|
||||
console.log('⚠️ Каталог не поддерживает полнотекстовый поиск')
|
||||
|
||||
// Сохраняем в историю поиска даже при отсутствии результатов
|
||||
await saveSearchHistory(
|
||||
context,
|
||||
searchQuery,
|
||||
'TEXT',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
0
|
||||
)
|
||||
|
||||
return {
|
||||
searchQuery: searchQuery,
|
||||
details: []
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Не удалось получить информацию о каталоге')
|
||||
}
|
||||
|
||||
const result = await laximoService.searchVehicleDetails(catalogCode, vehicleId, searchQuery, ssd)
|
||||
console.log('📋 Результат от LaximoService:', result ? `найдено ${result.details.length} деталей` : 'результат null')
|
||||
|
||||
// Сохраняем в историю поиска
|
||||
if (result) {
|
||||
await saveSearchHistory(
|
||||
context,
|
||||
searchQuery,
|
||||
'TEXT',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
result.details.length
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error('❌ Ошибка в GraphQL resolver поиска деталей по названию:', err)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
laximoDocFindOEM: async (_: unknown, { oemNumber, brand, replacementTypes }: { oemNumber: string; brand?: string; replacementTypes?: string }, context: Context) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - Doc FindOEM поиск по артикулу:', { oemNumber, brand, replacementTypes })
|
||||
|
||||
const result = await laximoDocService.findOEM(oemNumber, brand, replacementTypes)
|
||||
console.log('📋 Результат от LaximoDocService:', result ? `найдено ${result.details.length} деталей` : 'результат null')
|
||||
|
||||
// Сохраняем в историю поиска
|
||||
if (result) {
|
||||
await saveSearchHistory(
|
||||
context,
|
||||
oemNumber,
|
||||
'OEM',
|
||||
brand,
|
||||
oemNumber,
|
||||
undefined,
|
||||
result.details.length
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error('❌ Ошибка в GraphQL resolver Doc FindOEM:', err)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// Резолверы для работы с деталями узлов
|
||||
laximoUnitInfo: async (_: unknown, { catalogCode, vehicleId, unitId, ssd }: { catalogCode: string; vehicleId: string; unitId: string; ssd: string }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - получение информации об узле:', { catalogCode, vehicleId, unitId })
|
||||
|
||||
const result = await laximoUnitService.getUnitInfo(catalogCode, vehicleId, unitId, ssd)
|
||||
console.log('📋 Результат от LaximoUnitService:', result ? `найден узел ${result.name}` : 'узел не найден')
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error('❌ Ошибка в GraphQL resolver UnitInfo:', err)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
laximoUnitDetails: async (_: unknown, { catalogCode, vehicleId, unitId, ssd }: { catalogCode: string; vehicleId: string; unitId: string; ssd: string }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - получение деталей узла:', { catalogCode, vehicleId, unitId })
|
||||
|
||||
const result = await laximoUnitService.getUnitDetails(catalogCode, vehicleId, unitId, ssd)
|
||||
console.log('📋 Результат от LaximoUnitService:', result ? `найдено ${result.length} деталей` : 'детали не найдены')
|
||||
|
||||
return result || []
|
||||
} catch (err) {
|
||||
console.error('❌ Ошибка в GraphQL resolver UnitDetails:', err)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
laximoUnitImageMap: async (_: unknown, { catalogCode, vehicleId, unitId, ssd }: { catalogCode: string; vehicleId: string; unitId: string; ssd: string }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - получение карты изображений узла:', { catalogCode, vehicleId, unitId })
|
||||
|
||||
const result = await laximoUnitService.getUnitImageMap(catalogCode, vehicleId, unitId, ssd)
|
||||
console.log('📋 Результат от LaximoUnitService:', result ? `найдена карта с ${result.coordinates.length} координатами` : 'карта не найдена')
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error('❌ Ошибка в GraphQL resolver UnitImageMap:', err)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// Поиск товаров и предложений
|
||||
searchProductOffers: async (_: unknown, {
|
||||
articleNumber,
|
||||
brand
|
||||
}: {
|
||||
articleNumber: string;
|
||||
brand: string;
|
||||
}, context: Context) => {
|
||||
try {
|
||||
// Проверяем входные параметры
|
||||
if (!articleNumber || !brand || articleNumber.trim() === '' || brand.trim() === '') {
|
||||
console.log('❌ GraphQL Resolver - некорректные параметры:', { articleNumber, brand })
|
||||
return {
|
||||
articleNumber: articleNumber || '',
|
||||
brand: brand || '',
|
||||
name: 'По запросу',
|
||||
internalOffers: [],
|
||||
externalOffers: [],
|
||||
analogs: [],
|
||||
hasInternalStock: false,
|
||||
totalOffers: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем параметры
|
||||
const cleanArticleNumber = articleNumber.trim()
|
||||
const cleanBrand = brand.trim()
|
||||
|
||||
console.log('🔍 GraphQL Resolver - поиск предложений для товара:', { articleNumber: cleanArticleNumber, brand: cleanBrand })
|
||||
|
||||
// 1. Поиск в нашей базе данных
|
||||
const internalProducts = await prisma.product.findMany({
|
||||
where: {
|
||||
article: {
|
||||
equals: cleanArticleNumber,
|
||||
mode: 'insensitive'
|
||||
}
|
||||
},
|
||||
include: {
|
||||
categories: true,
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
characteristics: { include: { characteristic: true } }
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`📦 Найдено ${internalProducts.length} товаров в нашей базе`)
|
||||
|
||||
// 2. Поиск в AutoEuro
|
||||
let externalOffers: any[] = []
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - начинаем поиск в AutoEuro:', { articleNumber: cleanArticleNumber, brand: cleanBrand })
|
||||
|
||||
const autoEuroResult = await autoEuroService.searchItems({
|
||||
code: cleanArticleNumber,
|
||||
brand: cleanBrand,
|
||||
with_crosses: false,
|
||||
with_offers: true
|
||||
})
|
||||
|
||||
console.log('📊 GraphQL Resolver - результат AutoEuro:', {
|
||||
success: autoEuroResult.success,
|
||||
dataLength: autoEuroResult.data?.length || 0,
|
||||
error: autoEuroResult.error
|
||||
})
|
||||
|
||||
if (autoEuroResult.success && autoEuroResult.data) {
|
||||
console.log('✅ GraphQL Resolver - обрабатываем данные AutoEuro, количество:', autoEuroResult.data.length)
|
||||
|
||||
externalOffers = autoEuroResult.data.map(offer => ({
|
||||
offerKey: offer.offer_key,
|
||||
brand: offer.brand,
|
||||
code: offer.code,
|
||||
name: offer.name,
|
||||
price: parseFloat(offer.price.toString()),
|
||||
currency: offer.currency || 'RUB',
|
||||
deliveryTime: calculateDeliveryDays(offer.delivery_time || ''),
|
||||
deliveryTimeMax: calculateDeliveryDays(offer.delivery_time_max || ''),
|
||||
quantity: offer.amount || 0,
|
||||
warehouse: offer.warehouse_name || 'Внешний склад',
|
||||
warehouseName: offer.warehouse_name || null,
|
||||
rejects: offer.rejects || 0,
|
||||
supplier: 'AutoEuro',
|
||||
canPurchase: true
|
||||
}))
|
||||
|
||||
console.log('🎯 GraphQL Resolver - создано внешних предложений:', externalOffers.length)
|
||||
} else {
|
||||
console.log('❌ GraphQL Resolver - AutoEuro не вернул данные:', autoEuroResult)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка поиска в AutoEuro:', error)
|
||||
}
|
||||
|
||||
console.log(`🌐 Найдено ${externalOffers.length} предложений в AutoEuro`)
|
||||
console.log('📦 Первые 3 внешних предложения:', externalOffers.slice(0, 3))
|
||||
|
||||
// 3. Поиск в PartsIndex для получения дополнительных характеристик и изображений
|
||||
let partsIndexData: any = null
|
||||
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - прямой поиск в PartsIndex:', {
|
||||
articleNumber: cleanArticleNumber,
|
||||
brand: cleanBrand
|
||||
})
|
||||
|
||||
// Используем прямой поиск по артикулу и бренду
|
||||
partsIndexData = await partsIndexService.searchEntityByCode(
|
||||
cleanArticleNumber,
|
||||
cleanBrand
|
||||
)
|
||||
|
||||
if (partsIndexData) {
|
||||
console.log('✅ GraphQL Resolver - найден товар в PartsIndex:', {
|
||||
code: partsIndexData.code,
|
||||
brand: partsIndexData.brand?.name,
|
||||
images: partsIndexData.images?.length || 0,
|
||||
parameters: partsIndexData.parameters?.length || 0
|
||||
})
|
||||
} else {
|
||||
console.log('⚠️ GraphQL Resolver - товар не найден в PartsIndex')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка поиска в PartsIndex:', error)
|
||||
// Не бросаем ошибку, просто продолжаем без данных PartsIndex
|
||||
console.log('⚠️ Продолжаем без данных PartsIndex из-за ошибки API')
|
||||
}
|
||||
|
||||
// 4. Поиск аналогов через AutoEuro (используем кроссы)
|
||||
const analogs: any[] = []
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - поиск аналогов через AutoEuro с кроссами')
|
||||
|
||||
const analogsResult = await autoEuroService.searchItems({
|
||||
code: cleanArticleNumber,
|
||||
brand: cleanBrand,
|
||||
with_crosses: true, // Включаем кроссы для получения аналогов
|
||||
with_offers: false
|
||||
})
|
||||
|
||||
if (analogsResult.success && analogsResult.data) {
|
||||
console.log('✅ GraphQL Resolver - найдены аналоги через AutoEuro:', analogsResult.data.length)
|
||||
|
||||
// Фильтруем только кроссы и аналоги (не оригинальный товар)
|
||||
// Убираем дубликаты по комбинации brand + articleNumber
|
||||
const uniqueAnalogs = new Map<string, any>()
|
||||
|
||||
analogsResult.data
|
||||
.filter(item => item.cross !== null && item.cross !== undefined)
|
||||
.forEach(item => {
|
||||
const key = `${item.brand}-${item.code}`
|
||||
if (!uniqueAnalogs.has(key)) {
|
||||
const crossType = Number(item.cross)
|
||||
uniqueAnalogs.set(key, {
|
||||
brand: item.brand,
|
||||
articleNumber: item.code,
|
||||
name: item.name,
|
||||
type: crossType === 0 ? 'Кросс' :
|
||||
crossType === 1 ? 'Замена номера' :
|
||||
crossType === 2 ? 'Синоним бренда' :
|
||||
crossType === 3 ? 'Проверенный кросс' :
|
||||
crossType === 10 ? 'Комплект' :
|
||||
crossType === 11 ? 'Часть' :
|
||||
crossType === 12 ? 'Тюнинг' : 'Аналог'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Берем первые 5 уникальных аналогов
|
||||
const analogsFromAutoEuro = Array.from(uniqueAnalogs.values()).slice(0, 5)
|
||||
|
||||
analogs.push(...analogsFromAutoEuro)
|
||||
console.log('🎯 GraphQL Resolver - добавлено аналогов из AutoEuro:', analogsFromAutoEuro.length)
|
||||
} else {
|
||||
console.log('⚠️ GraphQL Resolver - AutoEuro не вернул аналоги')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка поиска аналогов через AutoEuro:', error)
|
||||
// Не бросаем ошибку, просто продолжаем без аналогов
|
||||
console.log('⚠️ Продолжаем без поиска аналогов из-за ошибки API')
|
||||
}
|
||||
|
||||
console.log(`🔄 Найдено ${analogs.length} аналогов`)
|
||||
|
||||
// 5. Формируем внутренние предложения
|
||||
const internalOffers = internalProducts.map(product => ({
|
||||
id: product.id,
|
||||
productId: product.id,
|
||||
price: product.retailPrice || 0,
|
||||
quantity: product.stock || 0,
|
||||
warehouse: 'Основной склад',
|
||||
deliveryDays: 1,
|
||||
available: (product.stock || 0) > 0,
|
||||
rating: 4.8,
|
||||
supplier: 'Protek'
|
||||
}))
|
||||
|
||||
// 6. Определяем название товара и собираем данные
|
||||
let productName = ''
|
||||
let productDescription = ''
|
||||
let productImages: any[] = []
|
||||
let productCharacteristics: any[] = []
|
||||
let partsIndexImages: any[] = []
|
||||
let partsIndexCharacteristics: any[] = []
|
||||
|
||||
// Приоритет: внутренняя база -> PartsIndex -> AutoEuro
|
||||
if (internalProducts.length > 0) {
|
||||
const firstProduct = internalProducts[0]
|
||||
productName = firstProduct.name
|
||||
productDescription = firstProduct.description || ''
|
||||
productImages = firstProduct.images
|
||||
productCharacteristics = firstProduct.characteristics
|
||||
}
|
||||
|
||||
// Добавляем данные из PartsIndex
|
||||
if (partsIndexData) {
|
||||
if (!productName) {
|
||||
productName = partsIndexData.name?.name || partsIndexData.originalName || `${cleanBrand} ${cleanArticleNumber}`
|
||||
}
|
||||
|
||||
if (!productDescription && partsIndexData.description) {
|
||||
productDescription = partsIndexData.description
|
||||
}
|
||||
|
||||
// Добавляем изображения из PartsIndex
|
||||
if (partsIndexData.images && Array.isArray(partsIndexData.images)) {
|
||||
partsIndexImages = partsIndexData.images.map((imageUrl: string, index: number) => ({
|
||||
url: imageUrl,
|
||||
alt: `${productName} - изображение ${index + 1}`,
|
||||
order: index + 1,
|
||||
source: 'PartsIndex'
|
||||
}))
|
||||
}
|
||||
|
||||
// Добавляем характеристики из PartsIndex
|
||||
if (partsIndexData.parameters && Array.isArray(partsIndexData.parameters)) {
|
||||
partsIndexCharacteristics = partsIndexData.parameters.flatMap((paramGroup: any) =>
|
||||
paramGroup.params ? paramGroup.params.map((param: any) => ({
|
||||
name: param.title || param.name || 'Характеристика',
|
||||
value: param.values && param.values.length > 0 ? param.values.map((v: any) => v.value).join(', ') : 'Не указано',
|
||||
source: 'PartsIndex'
|
||||
})) : []
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Если нет названия, используем данные из AutoEuro
|
||||
if (!productName && externalOffers.length > 0) {
|
||||
productName = externalOffers[0].name
|
||||
}
|
||||
|
||||
// Если все еще нет названия, формируем из бренда и артикула
|
||||
if (!productName) {
|
||||
productName = `${cleanBrand} ${cleanArticleNumber}`
|
||||
}
|
||||
|
||||
const result = {
|
||||
articleNumber: cleanArticleNumber,
|
||||
brand: cleanBrand,
|
||||
name: productName,
|
||||
description: productDescription,
|
||||
images: productImages,
|
||||
characteristics: productCharacteristics,
|
||||
partsIndexImages,
|
||||
partsIndexCharacteristics,
|
||||
internalOffers,
|
||||
externalOffers,
|
||||
analogs,
|
||||
hasInternalStock: internalOffers.some(offer => offer.available),
|
||||
totalOffers: internalOffers.length + externalOffers.length
|
||||
}
|
||||
|
||||
console.log('✅ Результат поиска предложений:', {
|
||||
articleNumber: cleanArticleNumber,
|
||||
brand: cleanBrand,
|
||||
internalOffers: result.internalOffers.length,
|
||||
externalOffers: result.externalOffers.length,
|
||||
analogs: result.analogs.length,
|
||||
hasInternalStock: result.hasInternalStock
|
||||
})
|
||||
|
||||
console.log('🔍 Детали результата:')
|
||||
console.log('- Внутренние предложения:', result.internalOffers)
|
||||
console.log('- Внешние предложения:', result.externalOffers.slice(0, 3))
|
||||
console.log('- Аналоги:', result.analogs.length)
|
||||
|
||||
// Сохраняем в историю поиска
|
||||
await saveSearchHistory(
|
||||
context,
|
||||
`${cleanBrand} ${cleanArticleNumber}`,
|
||||
'ARTICLE',
|
||||
cleanBrand,
|
||||
cleanArticleNumber,
|
||||
undefined,
|
||||
result.totalOffers
|
||||
)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver searchProductOffers:', error)
|
||||
throw new Error('Не удалось найти предложения для товара')
|
||||
}
|
||||
},
|
||||
|
||||
getAnalogOffers: async (_: unknown, { analogs }: { analogs: { articleNumber: string; brand: string; name?: string; type?: string }[] }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - поиск предложений для аналогов:', { count: analogs.length })
|
||||
|
||||
const analogPromises = analogs.map(async (analog) => {
|
||||
const { articleNumber, brand } = analog
|
||||
|
||||
// Поиск в нашей базе
|
||||
const analogInternalProducts = await prisma.product.findMany({
|
||||
where: { article: { equals: articleNumber, mode: 'insensitive' } },
|
||||
})
|
||||
|
||||
// Формируем внутренние предложения
|
||||
const internalOffers = analogInternalProducts.map(product => ({
|
||||
id: product.id,
|
||||
productId: product.id,
|
||||
price: product.retailPrice || 0,
|
||||
quantity: product.stock || 0,
|
||||
warehouse: 'Основной склад',
|
||||
deliveryDays: 1,
|
||||
available: (product.stock || 0) > 0,
|
||||
rating: 4.8,
|
||||
supplier: 'Protek'
|
||||
}))
|
||||
|
||||
// Поиск в AutoEuro только для аналогов без внутренних предложений
|
||||
let analogExternalOffers: any[] = []
|
||||
if (internalOffers.length === 0) {
|
||||
try {
|
||||
const analogAutoEuroResult = await autoEuroService.searchItems({
|
||||
code: articleNumber,
|
||||
brand: brand,
|
||||
with_crosses: false,
|
||||
with_offers: true,
|
||||
})
|
||||
|
||||
if (analogAutoEuroResult.success && analogAutoEuroResult.data) {
|
||||
analogExternalOffers = analogAutoEuroResult.data
|
||||
.slice(0, 3) // Ограничиваем до 3 лучших предложений
|
||||
.map((offer) => ({
|
||||
offerKey: offer.offer_key,
|
||||
brand: offer.brand,
|
||||
code: offer.code,
|
||||
name: offer.name,
|
||||
price: parseFloat(offer.price.toString()),
|
||||
currency: offer.currency || 'RUB',
|
||||
deliveryTime: calculateDeliveryDays(offer.delivery_time || ''),
|
||||
deliveryTimeMax: calculateDeliveryDays(offer.delivery_time_max || ''),
|
||||
quantity: offer.amount || 0,
|
||||
warehouse: offer.warehouse_name || 'Внешний склад',
|
||||
warehouseName: offer.warehouse_name || null,
|
||||
rejects: offer.rejects || 0,
|
||||
supplier: 'AutoEuro',
|
||||
canPurchase: true,
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Ошибка поиска аналога ${articleNumber} в AutoEuro:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Определяем название товара
|
||||
let name = analog.name || `${brand} ${articleNumber}` // Используем имя из аналога, если есть
|
||||
if (analogInternalProducts.length > 0) {
|
||||
name = analogInternalProducts[0].name
|
||||
} else if (analogExternalOffers.length > 0 && analogExternalOffers[0].name) {
|
||||
name = analogExternalOffers[0].name
|
||||
}
|
||||
|
||||
return {
|
||||
articleNumber,
|
||||
brand,
|
||||
name,
|
||||
type: analog.type || 'Аналог',
|
||||
internalOffers,
|
||||
externalOffers: analogExternalOffers,
|
||||
}
|
||||
})
|
||||
|
||||
const analogResults = await Promise.all(analogPromises)
|
||||
console.log('✅ GraphQL Resolver - поиск аналогов завершен:', {
|
||||
processedAnalogs: analogResults.length,
|
||||
totalOffers: analogResults.reduce((sum, result) => sum + (result.internalOffers?.length || 0) + (result.externalOffers?.length || 0), 0),
|
||||
})
|
||||
|
||||
return analogResults
|
||||
} catch (error) {
|
||||
console.error('❌ GraphQL Resolver - ошибка поиска аналогов:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
getBrandsByCode: async (_: unknown, { code }: { code: string }, context: Context) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - поиск брендов по коду:', { code })
|
||||
|
||||
if (!code || code.trim() === '') {
|
||||
console.log('❌ GraphQL Resolver - некорректный код:', { code })
|
||||
return {
|
||||
success: false,
|
||||
error: 'Код артикула не может быть пустым',
|
||||
brands: []
|
||||
}
|
||||
}
|
||||
|
||||
const cleanCode = code.trim()
|
||||
console.log('🔍 GraphQL Resolver - начинаем поиск брендов в AutoEuro:', { code: cleanCode })
|
||||
|
||||
const autoEuroResult = await autoEuroService.getBrandsByCode(cleanCode)
|
||||
|
||||
console.log('📊 GraphQL Resolver - результат поиска брендов AutoEuro:', {
|
||||
success: autoEuroResult.success,
|
||||
brandsCount: autoEuroResult.data?.length || 0,
|
||||
error: autoEuroResult.error
|
||||
})
|
||||
|
||||
if (autoEuroResult.success && autoEuroResult.data) {
|
||||
console.log('✅ GraphQL Resolver - найдены бренды:', autoEuroResult.data.length)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
brands: autoEuroResult.data,
|
||||
error: null
|
||||
}
|
||||
} else {
|
||||
console.log('❌ GraphQL Resolver - AutoEuro не вернул бренды:', autoEuroResult)
|
||||
return {
|
||||
success: false,
|
||||
error: autoEuroResult.error || 'Бренды не найдены',
|
||||
brands: []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ GraphQL Resolver - ошибка поиска брендов:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Неизвестная ошибка',
|
||||
brands: []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getCategoryProductsWithOffers: async (_: unknown, {
|
||||
categoryName,
|
||||
excludeArticle,
|
||||
excludeBrand,
|
||||
limit = 5
|
||||
}: {
|
||||
categoryName: string;
|
||||
excludeArticle: string;
|
||||
excludeBrand: string;
|
||||
limit?: number
|
||||
}) => {
|
||||
// Функция для определения ключевых слов категории
|
||||
const getCategoryKeywords = (categoryName: string): string[] => {
|
||||
const name = categoryName.toLowerCase()
|
||||
|
||||
// Словарь категорий и их ключевых слов
|
||||
const categoryMappings: { [key: string]: string[] } = {
|
||||
'шины': ['шина', 'покрышка', 'резина', 'tire'],
|
||||
'масла': ['масло', 'oil', 'жидкость'],
|
||||
'фильтры': ['фильтр', 'filter'],
|
||||
'тормоза': ['тормоз', 'brake', 'колодка', 'диск'],
|
||||
'аккумуляторы': ['аккумулятор', 'battery', 'батарея'],
|
||||
'свечи': ['свеча', 'spark', 'зажигание'],
|
||||
'стартеры': ['стартер', 'starter'],
|
||||
'генераторы': ['генератор', 'alternator'],
|
||||
'амортизаторы': ['амортизатор', 'shock', 'стойка']
|
||||
}
|
||||
|
||||
// Ищем совпадения
|
||||
for (const [category, keywords] of Object.entries(categoryMappings)) {
|
||||
if (name.includes(category)) {
|
||||
return keywords
|
||||
}
|
||||
}
|
||||
|
||||
// Если категория не найдена, используем само название
|
||||
return [name]
|
||||
}
|
||||
|
||||
// Функция для извлечения бренда из названия товара
|
||||
const extractBrandFromName = (productName: string): string => {
|
||||
const name = productName.trim()
|
||||
const words = name.split(' ')
|
||||
|
||||
// Обычно бренд - это первое слово
|
||||
if (words.length > 0) {
|
||||
return words[0]
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - поиск товаров категории с предложениями:', {
|
||||
categoryName,
|
||||
excludeArticle,
|
||||
excludeBrand,
|
||||
limit
|
||||
})
|
||||
|
||||
// 1. Определяем ключевые слова для поиска товаров из категории
|
||||
const categoryKeywords = getCategoryKeywords(categoryName)
|
||||
console.log('🏷️ Ключевые слова категории:', categoryKeywords)
|
||||
|
||||
// 2. Поиск товаров в нашей базе данных по ключевым словам
|
||||
const internalProducts = await prisma.product.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
// Исключаем текущий товар
|
||||
{
|
||||
NOT: {
|
||||
AND: [
|
||||
{ article: { equals: excludeArticle, mode: 'insensitive' } },
|
||||
{ name: { contains: excludeBrand, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
},
|
||||
// Поиск по ключевым словам категории
|
||||
{
|
||||
OR: categoryKeywords.map(keyword => ({
|
||||
OR: [
|
||||
{ name: { contains: keyword, mode: 'insensitive' } },
|
||||
{ description: { contains: keyword, mode: 'insensitive' } },
|
||||
{ categories: { some: { name: { contains: keyword, mode: 'insensitive' } } } }
|
||||
]
|
||||
}))
|
||||
}
|
||||
]
|
||||
},
|
||||
include: {
|
||||
categories: true
|
||||
},
|
||||
take: limit * 3 // Берем больше товаров для проверки наличия предложений
|
||||
})
|
||||
|
||||
console.log(`📦 Найдено ${internalProducts.length} товаров в категории из нашей базы`)
|
||||
|
||||
// 3. Проверяем наличие предложений AutoEuro для каждого товара
|
||||
const productsWithOffers: any[] = []
|
||||
|
||||
for (const product of internalProducts) {
|
||||
if (productsWithOffers.length >= limit) break
|
||||
|
||||
// Извлекаем бренд из названия товара (обычно первое слово)
|
||||
const productBrand = extractBrandFromName(product.name)
|
||||
|
||||
if (!product.article || !productBrand) {
|
||||
console.log('⚠️ Пропускаем товар без артикула или бренда:', product.name)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем наличие предложений в AutoEuro
|
||||
const autoEuroResult = await autoEuroService.searchItems({
|
||||
code: product.article,
|
||||
brand: productBrand,
|
||||
with_crosses: false,
|
||||
with_offers: true
|
||||
})
|
||||
|
||||
if (autoEuroResult.success && autoEuroResult.data && autoEuroResult.data.length > 0) {
|
||||
// Находим минимальную цену
|
||||
const minPrice = Math.min(...autoEuroResult.data.map(offer => parseFloat(offer.price.toString())))
|
||||
|
||||
productsWithOffers.push({
|
||||
articleNumber: product.article,
|
||||
brand: productBrand,
|
||||
name: product.name,
|
||||
artId: product.id, // Используем ID товара как artId
|
||||
minPrice,
|
||||
hasOffers: true
|
||||
})
|
||||
|
||||
console.log('✅ Товар с предложениями:', {
|
||||
article: product.article,
|
||||
brand: productBrand,
|
||||
name: product.name,
|
||||
minPrice,
|
||||
offersCount: autoEuroResult.data.length
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Ошибка проверки предложений для ${product.article}:`, error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🎯 Итого найдено товаров с предложениями: ${productsWithOffers.length}`)
|
||||
|
||||
return productsWithOffers.slice(0, limit)
|
||||
} catch (error) {
|
||||
console.error('❌ GraphQL Resolver - ошибка поиска товаров категории:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
// PartsAPI категории
|
||||
partsAPICategories: async (_: unknown, { carId, carType = 'PC' }: { carId: number; carType?: 'PC' | 'CV' | 'Motorcycle' }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - PartsAPI категории:', { carId, carType });
|
||||
|
||||
const categories = await partsAPIService.getSearchTree(carId, carType);
|
||||
|
||||
console.log('✅ GraphQL Resolver - получено категорий PartsAPI:', categories.length);
|
||||
|
||||
return categories;
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsAPICategories:', error)
|
||||
throw new Error('Не удалось получить категории PartsAPI')
|
||||
}
|
||||
},
|
||||
|
||||
partsAPITopLevelCategories: async (_: unknown, { carId, carType = 'PC' }: { carId: number; carType?: 'PC' | 'CV' | 'Motorcycle' }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - PartsAPI категории верхнего уровня:', { carId, carType });
|
||||
|
||||
const tree = await partsAPIService.getSearchTree(carId, carType);
|
||||
const categories = partsAPIService.getTopLevelCategories(tree);
|
||||
|
||||
console.log('✅ GraphQL Resolver - получено категорий верхнего уровня PartsAPI:', categories.length);
|
||||
|
||||
return categories;
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsAPITopLevelCategories:', error)
|
||||
throw new Error('Не удалось получить категории верхнего уровня PartsAPI')
|
||||
}
|
||||
},
|
||||
|
||||
partsAPIRootCategories: async (_: unknown, { carId, carType = 'PC' }: { carId: number; carType?: 'PC' | 'CV' | 'Motorcycle' }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - PartsAPI корневые категории:', { carId, carType });
|
||||
|
||||
const tree = await partsAPIService.getSearchTree(carId, carType);
|
||||
const categories = partsAPIService.getRootCategories(tree);
|
||||
|
||||
console.log('✅ GraphQL Resolver - получено корневых категорий PartsAPI:', categories.length);
|
||||
|
||||
return categories;
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsAPIRootCategories:', error)
|
||||
throw new Error('Не удалось получить корневые категории PartsAPI')
|
||||
}
|
||||
},
|
||||
|
||||
// PartsIndex категории автотоваров
|
||||
partsIndexCatalogs: async (_: unknown, { lang = 'ru' }: { lang?: 'ru' | 'en' }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - PartsIndex каталоги:', { lang });
|
||||
|
||||
const catalogs = await partsIndexService.getCatalogs(lang);
|
||||
|
||||
console.log('✅ GraphQL Resolver - получено каталогов PartsIndex:', catalogs.length);
|
||||
|
||||
return catalogs.map(catalog => ({
|
||||
...catalog,
|
||||
groups: [] // Пустой массив групп, если нужны группы - используйте другой запрос
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsIndexCatalogs:', error)
|
||||
throw new Error('Не удалось получить каталоги PartsIndex')
|
||||
}
|
||||
},
|
||||
|
||||
partsIndexCatalogGroups: async (_: unknown, { catalogId, lang = 'ru' }: { catalogId: string; lang?: 'ru' | 'en' }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - PartsIndex группы каталога:', { catalogId, lang });
|
||||
|
||||
const groups = await partsIndexService.getCatalogGroups(catalogId, lang);
|
||||
|
||||
console.log('✅ GraphQL Resolver - получено групп PartsIndex:', groups.length);
|
||||
|
||||
return groups;
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsIndexCatalogGroups:', error)
|
||||
throw new Error('Не удалось получить группы каталога PartsIndex')
|
||||
}
|
||||
},
|
||||
|
||||
partsIndexCategoriesWithGroups: async (_: unknown, { lang = 'ru' }: { lang?: 'ru' | 'en' }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - PartsIndex категории с группами:', { lang });
|
||||
|
||||
const categoriesWithGroups = await partsIndexService.getCategoriesWithGroups(lang);
|
||||
|
||||
console.log('✅ GraphQL Resolver - получено категорий с группами PartsIndex:', categoriesWithGroups.length);
|
||||
|
||||
return categoriesWithGroups;
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsIndexCategoriesWithGroups:', error)
|
||||
throw new Error('Не удалось получить категории с группами PartsIndex')
|
||||
}
|
||||
},
|
||||
|
||||
partsIndexCatalogEntities: async (_: unknown, {
|
||||
catalogId,
|
||||
groupId,
|
||||
lang = 'ru',
|
||||
limit = 25,
|
||||
page = 1,
|
||||
q,
|
||||
engineId,
|
||||
generationId,
|
||||
params
|
||||
}: {
|
||||
catalogId: string;
|
||||
groupId: string;
|
||||
lang?: 'ru' | 'en';
|
||||
limit?: number;
|
||||
page?: number;
|
||||
q?: string;
|
||||
engineId?: string;
|
||||
generationId?: string;
|
||||
params?: string;
|
||||
}) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL resolver partsIndexCatalogEntities вызван с параметрами:', {
|
||||
catalogId,
|
||||
groupId,
|
||||
lang,
|
||||
limit,
|
||||
page,
|
||||
q
|
||||
})
|
||||
|
||||
// Преобразуем строку params в объект если передан
|
||||
let parsedParams: Record<string, any> | undefined;
|
||||
if (params) {
|
||||
try {
|
||||
parsedParams = JSON.parse(params);
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Не удалось разобрать параметры фильтрации:', params);
|
||||
}
|
||||
}
|
||||
|
||||
const entities = await partsIndexService.getCatalogEntities(catalogId, groupId, {
|
||||
lang,
|
||||
limit,
|
||||
page,
|
||||
q,
|
||||
engineId,
|
||||
generationId,
|
||||
params: parsedParams
|
||||
})
|
||||
|
||||
if (!entities) {
|
||||
console.warn('⚠️ Не удалось получить товары каталога')
|
||||
return {
|
||||
pagination: {
|
||||
limit,
|
||||
page: {
|
||||
prev: page > 1 ? page - 1 : 0,
|
||||
current: page,
|
||||
next: 0
|
||||
}
|
||||
},
|
||||
list: [],
|
||||
catalog: {
|
||||
id: catalogId,
|
||||
name: 'Неизвестная категория',
|
||||
image: '',
|
||||
groups: []
|
||||
},
|
||||
subgroup: null
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Получены товары каталога:', entities.list.length)
|
||||
|
||||
return entities
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsIndexCatalogEntities:', error)
|
||||
throw new Error('Не удалось получить товары каталога')
|
||||
}
|
||||
},
|
||||
|
||||
partsIndexCatalogParams: async (_: unknown, {
|
||||
catalogId,
|
||||
groupId,
|
||||
lang = 'ru',
|
||||
engineId,
|
||||
generationId,
|
||||
params,
|
||||
q
|
||||
}: {
|
||||
catalogId: string;
|
||||
groupId: string;
|
||||
lang?: 'ru' | 'en';
|
||||
engineId?: string;
|
||||
generationId?: string;
|
||||
params?: string;
|
||||
q?: string;
|
||||
}) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL resolver partsIndexCatalogParams вызван с параметрами:', {
|
||||
catalogId,
|
||||
groupId,
|
||||
lang,
|
||||
q
|
||||
})
|
||||
|
||||
// Преобразуем строку params в объект если передан
|
||||
let parsedParams: Record<string, any> | undefined;
|
||||
if (params) {
|
||||
try {
|
||||
parsedParams = JSON.parse(params);
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Не удалось разобрать параметры фильтрации:', params);
|
||||
}
|
||||
}
|
||||
|
||||
const parameters = await partsIndexService.getCatalogParams(catalogId, groupId, {
|
||||
lang,
|
||||
engineId,
|
||||
generationId,
|
||||
params: parsedParams,
|
||||
q
|
||||
})
|
||||
|
||||
if (!parameters) {
|
||||
console.warn('⚠️ Не удалось получить параметры каталога')
|
||||
return {
|
||||
list: [],
|
||||
paramsQuery: {}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Получены параметры каталога:', parameters.list.length)
|
||||
|
||||
return parameters
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsIndexCatalogParams:', error)
|
||||
throw new Error('Не удалось получить параметры каталога')
|
||||
}
|
||||
},
|
||||
|
||||
partsIndexSearchByArticle: async (_: unknown, {
|
||||
articleNumber,
|
||||
brandName,
|
||||
lang = 'ru'
|
||||
}: {
|
||||
articleNumber: string;
|
||||
brandName: string;
|
||||
lang?: 'ru' | 'en'
|
||||
}) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL resolver partsIndexSearchByArticle вызван с параметрами:', {
|
||||
articleNumber,
|
||||
brandName,
|
||||
lang
|
||||
})
|
||||
|
||||
// ВРЕМЕННО ОТКЛЮЧАЕМ ПОИСК В PARTSINDEX ДЛЯ КАРТОЧКИ ТОВАРА
|
||||
// чтобы избежать множественных запросов
|
||||
console.log('⚠️ Поиск в PartsIndex временно отключен для оптимизации')
|
||||
return null
|
||||
|
||||
/* ЗАКОММЕНТИРОВАННЫЙ КОД ДЛЯ БУДУЩЕГО ИСПОЛЬЗОВАНИЯ
|
||||
const entity = await partsIndexService.searchEntityByArticle(articleNumber, brandName, lang)
|
||||
|
||||
if (!entity) {
|
||||
console.log('❌ Товар не найден в Parts Index:', { articleNumber, brandName })
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('✅ Товар найден в Parts Index:', entity.code, entity.brand.name)
|
||||
|
||||
// Получаем детальную информацию о товаре
|
||||
// Поскольку у нас нет catalogId, попробуем найти товар через основные каталоги
|
||||
const catalogs = await partsIndexService.getCatalogs(lang)
|
||||
|
||||
for (const catalog of catalogs) {
|
||||
try {
|
||||
const entityDetail = await partsIndexService.getEntityById(catalog.id, entity.id, lang)
|
||||
|
||||
if (entityDetail) {
|
||||
console.log('✅ Получена детальная информация о товаре из каталога:', catalog.id)
|
||||
return entityDetail
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Ошибка получения детальной информации из каталога:', catalog.id, error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Если детальная информация не найдена, возвращаем базовую информацию
|
||||
console.log('⚠️ Детальная информация не найдена, возвращаем базовую')
|
||||
return {
|
||||
id: entity.id,
|
||||
catalog: {
|
||||
id: 'unknown',
|
||||
name: 'Неизвестный каталог',
|
||||
image: '',
|
||||
groups: []
|
||||
},
|
||||
subgroups: [],
|
||||
name: entity.name,
|
||||
originalName: entity.originalName,
|
||||
code: entity.code,
|
||||
barcodes: [],
|
||||
brand: entity.brand,
|
||||
description: '',
|
||||
parameters: entity.parameters.map(param => ({
|
||||
id: param.id,
|
||||
name: param.title,
|
||||
params: [{
|
||||
id: param.id,
|
||||
code: param.code,
|
||||
title: param.title,
|
||||
type: param.type,
|
||||
values: param.values
|
||||
}]
|
||||
})),
|
||||
images: entity.images,
|
||||
links: []
|
||||
}
|
||||
*/
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsIndexSearchByArticle:', error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// Получить детальную информацию о товаре PartsIndex по ID
|
||||
partsIndexGetEntityById: async (_: unknown, {
|
||||
catalogId,
|
||||
entityId,
|
||||
lang = 'ru'
|
||||
}: {
|
||||
catalogId: string;
|
||||
entityId: string;
|
||||
lang?: 'ru' | 'en'
|
||||
}) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL resolver partsIndexGetEntityById вызван с параметрами:', {
|
||||
catalogId,
|
||||
entityId,
|
||||
lang
|
||||
})
|
||||
|
||||
const entityDetail = await partsIndexService.getEntityById(catalogId, entityId, lang)
|
||||
|
||||
if (!entityDetail) {
|
||||
console.log('❌ Деталь товара не найдена в Parts Index:', { catalogId, entityId })
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('✅ Детальная информация товара получена из Parts Index:', entityDetail.code, entityDetail.brand.name)
|
||||
return entityDetail
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsIndexGetEntityById:', error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// PartsAPI артикулы
|
||||
partsAPIArticles: async (_: unknown, { strId, carId, carType = 'PC' }: { strId: number; carId: number; carType?: 'PC' | 'CV' | 'Motorcycle' }) => {
|
||||
try {
|
||||
console.log('🔍 GraphQL Resolver - PartsAPI артикулы:', { strId, carId, carType });
|
||||
|
||||
const articles = await partsAPIService.getArticles(strId, carId, carType);
|
||||
|
||||
console.log('✅ GraphQL Resolver - получено артикулов PartsAPI:', articles.length);
|
||||
|
||||
if (!articles || articles.length === 0) {
|
||||
console.log('⚠️ Артикулы для данной категории не найдены');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Преобразуем названия полей для соответствия GraphQL схеме с проверкой на null/undefined
|
||||
const transformedArticles = articles.map(article => ({
|
||||
supBrand: article.SUP_BRAND || '',
|
||||
supId: article.SUP_ID || 0,
|
||||
productGroup: article.PRODUCT_GROUP || '',
|
||||
ptId: article.PT_ID || 0,
|
||||
artSupBrand: article.ART_SUP_BRAND || '',
|
||||
artArticleNr: article.ART_ARTICLE_NR || '',
|
||||
artId: article.ART_ID || ''
|
||||
}));
|
||||
|
||||
return transformedArticles;
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка в GraphQL resolver partsAPIArticles:', error)
|
||||
// Возвращаем пустой массив вместо выброса ошибки
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// PartsAPI изображения
|
||||
partsAPIMedia: async (_: unknown, { artId, lang = 16 }: { artId: string; lang?: number }) => {
|
||||
try {
|
||||
console.log('🖼️ GraphQL Resolver - PartsAPI изображения:', { artId, lang });
|
||||
|
||||
const media = await partsAPIService.getArticleMedia(artId, lang);
|
||||
|
||||
console.log('✅ GraphQL Resolver - получено изображений PartsAPI:', media.length);
|
||||
|
||||
if (!media || media.length === 0) {
|
||||
console.log('⚠️ Изображения для артикула не найдены');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Преобразуем данные для GraphQL схемы
|
||||
const transformedMedia = media.map(item => ({
|
||||
artMediaType: String(item.ART_MEDIA_TYPE),
|
||||
artMediaSource: item.ART_MEDIA_SOURCE,
|
||||
artMediaSupId: item.ART_MEDIA_SUP_ID,
|
||||
artMediaKind: item.ART_MEDIA_KIND || null,
|
||||
imageUrl: partsAPIService.getImageUrl(item.ART_MEDIA_SOURCE)
|
||||
}));
|
||||
|
||||
return transformedMedia;
|
||||
} catch (error) {
|
||||
console.error('❌ GraphQL Resolver ошибка PartsAPI изображения:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// PartsAPI главное изображение
|
||||
partsAPIMainImage: async (_: unknown, { artId }: { artId: string }) => {
|
||||
try {
|
||||
console.log('🖼️ GraphQL Resolver - PartsAPI главное изображение:', { artId });
|
||||
|
||||
const imageUrl = await partsAPIService.getArticleMainImage(artId);
|
||||
|
||||
if (imageUrl) {
|
||||
console.log('✅ GraphQL Resolver - получено главное изображение PartsAPI');
|
||||
} else {
|
||||
console.log('⚠️ Главное изображение для артикула не найдено');
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
} catch (error) {
|
||||
console.error('❌ GraphQL Resolver ошибка PartsAPI главное изображение:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// Заказы и платежи
|
||||
orders: async (_: unknown, { clientId, status, search, limit = 50, offset = 0 }: {
|
||||
clientId?: string;
|
||||
status?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number
|
||||
}, context: Context) => {
|
||||
try {
|
||||
const where: any = {}
|
||||
|
||||
if (clientId) {
|
||||
where.clientId = clientId
|
||||
}
|
||||
|
||||
if (status) {
|
||||
where.status = status
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ orderNumber: { contains: search, mode: 'insensitive' } },
|
||||
{ clientName: { contains: search, mode: 'insensitive' } },
|
||||
{ clientEmail: { contains: search, mode: 'insensitive' } },
|
||||
{ clientPhone: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
|
||||
const [orders, total] = await Promise.all([
|
||||
prisma.order.findMany({
|
||||
where,
|
||||
include: {
|
||||
client: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
},
|
||||
payments: true
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
skip: offset
|
||||
}),
|
||||
prisma.order.count({ where })
|
||||
])
|
||||
|
||||
return {
|
||||
orders,
|
||||
total,
|
||||
hasMore: offset + limit < total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения заказов:', error)
|
||||
throw new Error('Не удалось получить заказы')
|
||||
}
|
||||
},
|
||||
|
||||
order: async (_: unknown, { id }: { id: string }) => {
|
||||
try {
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
client: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
},
|
||||
payments: true
|
||||
}
|
||||
})
|
||||
|
||||
return order
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения заказа:', error)
|
||||
throw new Error('Не удалось получить заказ')
|
||||
}
|
||||
},
|
||||
|
||||
orderByNumber: async (_: unknown, { orderNumber }: { orderNumber: string }) => {
|
||||
try {
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { orderNumber },
|
||||
include: {
|
||||
client: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
},
|
||||
payments: true
|
||||
}
|
||||
})
|
||||
|
||||
return order
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения заказа по номеру:', error)
|
||||
throw new Error('Не удалось получить заказ')
|
||||
}
|
||||
},
|
||||
|
||||
payments: async (_: unknown, { orderId, status }: { orderId?: string; status?: string }) => {
|
||||
try {
|
||||
const where: any = {}
|
||||
|
||||
if (orderId) {
|
||||
where.orderId = orderId
|
||||
}
|
||||
|
||||
if (status) {
|
||||
where.status = status
|
||||
}
|
||||
|
||||
const payments = await prisma.payment.findMany({
|
||||
where,
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
client: true,
|
||||
items: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
return payments
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения платежей:', error)
|
||||
throw new Error('Не удалось получить платежи')
|
||||
}
|
||||
},
|
||||
|
||||
payment: async (_: unknown, { id }: { id: string }) => {
|
||||
try {
|
||||
const payment = await prisma.payment.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
client: true,
|
||||
items: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return payment
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения платежа:', error)
|
||||
throw new Error('Не удалось получить платеж')
|
||||
}
|
||||
},
|
||||
|
||||
// Резолверы для Яндекс доставки
|
||||
yandexDetectLocation: async (_: unknown, { location }: { location: string }) => {
|
||||
try {
|
||||
const response = await yandexDeliveryService.detectLocation(location)
|
||||
return response.variants.map(variant => ({
|
||||
address: variant.address,
|
||||
geoId: variant.geo_id
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Ошибка определения местоположения:', error)
|
||||
throw new Error('Не удалось определить местоположение')
|
||||
}
|
||||
},
|
||||
|
||||
yandexPickupPoints: async (_: unknown, { filters }: { filters?: any }) => {
|
||||
try {
|
||||
const request: any = {}
|
||||
|
||||
if (filters) {
|
||||
if (filters.geoId) request.geo_id = filters.geoId
|
||||
if (filters.latitude && filters.longitude) {
|
||||
const radiusKm = filters.radiusKm || 10
|
||||
const radiusDegrees = radiusKm / 111
|
||||
|
||||
request.latitude = {
|
||||
from: filters.latitude - radiusDegrees,
|
||||
to: filters.latitude + radiusDegrees
|
||||
}
|
||||
request.longitude = {
|
||||
from: filters.longitude - radiusDegrees,
|
||||
to: filters.longitude + radiusDegrees
|
||||
}
|
||||
}
|
||||
if (filters.isYandexBranded !== undefined) request.is_yandex_branded = filters.isYandexBranded
|
||||
if (filters.isPostOffice !== undefined) request.is_post_office = filters.isPostOffice
|
||||
if (filters.type) request.type = filters.type
|
||||
}
|
||||
|
||||
const response = await yandexDeliveryService.getPickupPoints(request)
|
||||
|
||||
return response.points.map((point: YandexPickupPoint) => ({
|
||||
id: point.id,
|
||||
name: point.name,
|
||||
address: {
|
||||
fullAddress: point.address.full_address,
|
||||
locality: point.address.locality,
|
||||
street: point.address.street,
|
||||
house: point.address.house,
|
||||
building: point.address.building,
|
||||
apartment: point.address.apartment,
|
||||
postalCode: point.address.postal_code,
|
||||
comment: point.address.comment
|
||||
},
|
||||
contact: {
|
||||
phone: point.contact.phone,
|
||||
email: point.contact.email,
|
||||
firstName: point.contact.first_name,
|
||||
lastName: point.contact.last_name
|
||||
},
|
||||
position: {
|
||||
latitude: point.position.latitude,
|
||||
longitude: point.position.longitude
|
||||
},
|
||||
schedule: {
|
||||
restrictions: point.schedule.restrictions.map(restriction => ({
|
||||
days: restriction.days,
|
||||
timeFrom: {
|
||||
hours: restriction.time_from.hours,
|
||||
minutes: restriction.time_from.minutes
|
||||
},
|
||||
timeTo: {
|
||||
hours: restriction.time_to.hours,
|
||||
minutes: restriction.time_to.minutes
|
||||
}
|
||||
})),
|
||||
timeZone: point.schedule.time_zone
|
||||
},
|
||||
type: point.type,
|
||||
paymentMethods: point.payment_methods,
|
||||
instruction: point.instruction,
|
||||
isDarkStore: point.is_dark_store || false,
|
||||
isMarketPartner: point.is_market_partner || false,
|
||||
isPostOffice: point.is_post_office || false,
|
||||
isYandexBranded: point.is_yandex_branded || false,
|
||||
formattedSchedule: yandexDeliveryService.formatSchedule(point.schedule),
|
||||
typeLabel: yandexDeliveryService.getTypeLabel(point.type)
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения ПВЗ:', error)
|
||||
throw new Error('Не удалось получить список ПВЗ')
|
||||
}
|
||||
},
|
||||
|
||||
yandexPickupPointsByCity: async (_: unknown, { cityName }: { cityName: string }) => {
|
||||
try {
|
||||
console.log('Запрос ПВЗ для города:', cityName)
|
||||
const points = await yandexDeliveryService.getPickupPointsByCity(cityName)
|
||||
console.log('Получено ПВЗ:', points.length)
|
||||
if (points.length > 0) {
|
||||
console.log('Первый ПВЗ:', JSON.stringify(points[0], null, 2))
|
||||
}
|
||||
|
||||
// Если ПВЗ не найдены, возвращаем пустой массив
|
||||
if (points.length === 0) {
|
||||
console.log(`ПВЗ в городе "${cityName}" не найдены`)
|
||||
return [];
|
||||
}
|
||||
|
||||
return points.map(point => ({
|
||||
id: point.id,
|
||||
name: point.name,
|
||||
address: {
|
||||
fullAddress: point.address.full_address,
|
||||
locality: point.address.locality,
|
||||
street: point.address.street,
|
||||
house: point.address.house,
|
||||
building: point.address.building,
|
||||
apartment: point.address.apartment,
|
||||
postalCode: point.address.postal_code,
|
||||
comment: point.address.comment
|
||||
},
|
||||
contact: {
|
||||
phone: point.contact.phone,
|
||||
email: point.contact.email,
|
||||
firstName: point.contact.first_name,
|
||||
lastName: point.contact.last_name
|
||||
},
|
||||
position: {
|
||||
latitude: point.position.latitude,
|
||||
longitude: point.position.longitude
|
||||
},
|
||||
schedule: {
|
||||
restrictions: point.schedule.restrictions.map(restriction => ({
|
||||
days: restriction.days,
|
||||
timeFrom: {
|
||||
hours: restriction.time_from.hours,
|
||||
minutes: restriction.time_from.minutes
|
||||
},
|
||||
timeTo: {
|
||||
hours: restriction.time_to.hours,
|
||||
minutes: restriction.time_to.minutes
|
||||
}
|
||||
})),
|
||||
timeZone: point.schedule.time_zone
|
||||
},
|
||||
type: point.type,
|
||||
paymentMethods: point.payment_methods,
|
||||
instruction: point.instruction,
|
||||
isDarkStore: point.is_dark_store || false,
|
||||
isMarketPartner: point.is_market_partner || false,
|
||||
isPostOffice: point.is_post_office || false,
|
||||
isYandexBranded: point.is_yandex_branded || false,
|
||||
formattedSchedule: yandexDeliveryService.formatSchedule(point.schedule),
|
||||
typeLabel: yandexDeliveryService.getTypeLabel(point.type)
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения ПВЗ по городу:', error)
|
||||
throw new Error('Не удалось получить ПВЗ для указанного города')
|
||||
}
|
||||
},
|
||||
|
||||
yandexPickupPointsByCoordinates: async (_: unknown, { latitude, longitude, radiusKm }: { latitude: number; longitude: number; radiusKm?: number }) => {
|
||||
try {
|
||||
console.log('Запрос ПВЗ по координатам:', latitude, longitude, radiusKm)
|
||||
const points = await yandexDeliveryService.getPickupPointsByCoordinates(latitude, longitude, radiusKm)
|
||||
console.log('Получено ПВЗ по координатам:', points.length)
|
||||
|
||||
// Если ПВЗ не найдены, возвращаем пустой массив
|
||||
if (points.length === 0) {
|
||||
console.log(`ПВЗ по координатам ${latitude}, ${longitude} не найдены`)
|
||||
return [];
|
||||
}
|
||||
|
||||
return points.map(point => ({
|
||||
id: point.id,
|
||||
name: point.name,
|
||||
address: {
|
||||
fullAddress: point.address.full_address,
|
||||
locality: point.address.locality,
|
||||
street: point.address.street,
|
||||
house: point.address.house,
|
||||
building: point.address.building,
|
||||
apartment: point.address.apartment,
|
||||
postalCode: point.address.postal_code,
|
||||
comment: point.address.comment
|
||||
},
|
||||
contact: {
|
||||
phone: point.contact.phone,
|
||||
email: point.contact.email,
|
||||
firstName: point.contact.first_name,
|
||||
lastName: point.contact.last_name
|
||||
},
|
||||
position: {
|
||||
latitude: point.position.latitude,
|
||||
longitude: point.position.longitude
|
||||
},
|
||||
schedule: {
|
||||
restrictions: point.schedule.restrictions.map(restriction => ({
|
||||
days: restriction.days,
|
||||
timeFrom: {
|
||||
hours: restriction.time_from.hours,
|
||||
minutes: restriction.time_from.minutes
|
||||
},
|
||||
timeTo: {
|
||||
hours: restriction.time_to.hours,
|
||||
minutes: restriction.time_to.minutes
|
||||
}
|
||||
})),
|
||||
timeZone: point.schedule.time_zone
|
||||
},
|
||||
type: point.type,
|
||||
paymentMethods: point.payment_methods,
|
||||
instruction: point.instruction,
|
||||
isDarkStore: point.is_dark_store || false,
|
||||
isMarketPartner: point.is_market_partner || false,
|
||||
isPostOffice: point.is_post_office || false,
|
||||
isYandexBranded: point.is_yandex_branded || false,
|
||||
formattedSchedule: yandexDeliveryService.formatSchedule(point.schedule),
|
||||
typeLabel: yandexDeliveryService.getTypeLabel(point.type)
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения ПВЗ по координатам:', error)
|
||||
throw new Error('Не удалось получить ПВЗ по координатам')
|
||||
}
|
||||
},
|
||||
|
||||
// Автокомплит адресов
|
||||
addressSuggestions: async (_: unknown, { query }: { query: string }) => {
|
||||
try {
|
||||
console.log('Запрос автокомплита адресов:', query)
|
||||
const suggestions = await getAddressSuggestions(query)
|
||||
console.log('Получено предложений:', suggestions.length)
|
||||
return suggestions
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения предложений адресов:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ClientProfile: {
|
||||
_count: (parent: { _count?: { clients: number } }) => {
|
||||
return parent._count || { clients: 0 }
|
||||
}
|
||||
},
|
||||
|
||||
ClientLegalEntity: {
|
||||
bankDetails: async (parent: { id: string; bankDetails?: unknown[] }) => {
|
||||
// Если bankDetails не загружены, загружаем их из базы данных
|
||||
if (!parent.bankDetails) {
|
||||
const bankDetails = await prisma.clientBankDetails.findMany({
|
||||
where: { legalEntityId: parent.id }
|
||||
})
|
||||
return bankDetails || []
|
||||
}
|
||||
return parent.bankDetails || []
|
||||
}
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
createUser: async (_: unknown, { input }: { input: CreateUserInput }, context: Context) => {
|
||||
try {
|
||||
const { firstName, lastName, email, password, avatar, role } = input
|
||||
|
||||
// Проверяем, существует ли пользователь с таким email
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('Пользователь с таким email уже существует')
|
||||
}
|
||||
|
||||
// Хешируем пароль
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
// Создаем пользователя
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
avatar,
|
||||
role: role || 'USER'
|
||||
}
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.userId && context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.USER_CREATE,
|
||||
details: `${firstName} ${lastName} (${email})`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return user
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания пользователя:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать пользователя')
|
||||
}
|
||||
},
|
||||
|
||||
login: async (_: unknown, { input }: { input: LoginInput }, context: Context) => {
|
||||
try {
|
||||
const { email, password } = input
|
||||
|
||||
// Находим пользователя по email
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Неверный email или пароль')
|
||||
}
|
||||
|
||||
// Проверяем пароль
|
||||
const isValidPassword = await comparePasswords(password, user.password)
|
||||
if (!isValidPassword) {
|
||||
throw new Error('Неверный email или пароль')
|
||||
}
|
||||
|
||||
// Создаем JWT токен
|
||||
const token = createToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
})
|
||||
|
||||
// Логируем вход
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: user.id,
|
||||
action: AuditAction.USER_LOGIN,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка входа:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось войти в систему')
|
||||
}
|
||||
},
|
||||
|
||||
logout: async (_: unknown, __: unknown, context: Context) => {
|
||||
// Логируем выход
|
||||
if (context.userId && context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.USER_LOGOUT,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
updateProfile: async (_: unknown, { input }: { input: UpdateProfileInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Проверяем, если изменяется email, что он уникален
|
||||
if (input.email) {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: input.email,
|
||||
id: { not: context.userId }
|
||||
}
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('Пользователь с таким email уже существует')
|
||||
}
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: context.userId },
|
||||
data: {
|
||||
...(input.firstName && { firstName: input.firstName }),
|
||||
...(input.lastName && { lastName: input.lastName }),
|
||||
...(input.email && { email: input.email }),
|
||||
...(input.avatar && { avatar: input.avatar }),
|
||||
}
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PROFILE_UPDATE,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return updatedUser
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления профиля:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить профиль')
|
||||
}
|
||||
},
|
||||
|
||||
changePassword: async (_: unknown, { input }: { input: ChangePasswordInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: context.userId }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Пользователь не найден')
|
||||
}
|
||||
|
||||
// Проверяем текущий пароль
|
||||
const isValidPassword = await comparePasswords(input.currentPassword, user.password)
|
||||
if (!isValidPassword) {
|
||||
throw new Error('Неверный текущий пароль')
|
||||
}
|
||||
|
||||
// Хешируем новый пароль
|
||||
const hashedNewPassword = await hashPassword(input.newPassword)
|
||||
|
||||
// Обновляем пароль
|
||||
await prisma.user.update({
|
||||
where: { id: context.userId },
|
||||
data: { password: hashedNewPassword }
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PASSWORD_CHANGE,
|
||||
details: 'Собственный пароль',
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка смены пароля:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось сменить пароль')
|
||||
}
|
||||
},
|
||||
|
||||
uploadAvatar: async (_: unknown, { file }: { file: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: context.userId },
|
||||
data: { avatar: file }
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.AVATAR_UPLOAD,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return updatedUser
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки аватара:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось загрузить аватар')
|
||||
}
|
||||
},
|
||||
|
||||
// Админские мутации для управления пользователями
|
||||
updateUser: async (_: unknown, { id, input }: { id: string; input: UpdateUserInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId || context.userRole !== 'ADMIN') {
|
||||
throw new Error('Недостаточно прав для выполнения операции')
|
||||
}
|
||||
|
||||
// Получаем данные пользователя до изменения
|
||||
const oldUser = await prisma.user.findUnique({ where: { id } })
|
||||
if (!oldUser) {
|
||||
throw new Error('Пользователь не найден')
|
||||
}
|
||||
|
||||
// Проверяем, если изменяется email, что он уникален
|
||||
if (input.email) {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: input.email,
|
||||
id: { not: id }
|
||||
}
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('Пользователь с таким email уже существует')
|
||||
}
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(input.firstName && { firstName: input.firstName }),
|
||||
...(input.lastName && { lastName: input.lastName }),
|
||||
...(input.email && { email: input.email }),
|
||||
...(input.avatar !== undefined && { avatar: input.avatar }),
|
||||
...(input.role && { role: input.role }),
|
||||
}
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.USER_UPDATE,
|
||||
details: `${oldUser.firstName} ${oldUser.lastName} (${oldUser.email})`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return updatedUser
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления пользователя:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить пользователя')
|
||||
}
|
||||
},
|
||||
|
||||
deleteUser: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId || context.userRole !== 'ADMIN') {
|
||||
throw new Error('Недостаточно прав для выполнения операции')
|
||||
}
|
||||
|
||||
// Нельзя удалить самого себя
|
||||
if (context.userId === id) {
|
||||
throw new Error('Нельзя удалить собственный аккаунт')
|
||||
}
|
||||
|
||||
// Получаем данные пользователя перед удалением
|
||||
const userToDelete = await prisma.user.findUnique({ where: { id } })
|
||||
if (!userToDelete) {
|
||||
throw new Error('Пользователь не найден')
|
||||
}
|
||||
|
||||
// Проверяем и обрабатываем связанные записи
|
||||
// 1. Обнуляем userId в client_balance_history (вместо удаления истории)
|
||||
await prisma.clientBalanceHistory.updateMany({
|
||||
where: { userId: id },
|
||||
data: { userId: null }
|
||||
})
|
||||
|
||||
// 2. Обнуляем managerId в таблице clients (переназначаем менеджера)
|
||||
await prisma.client.updateMany({
|
||||
where: { managerId: id },
|
||||
data: { managerId: null }
|
||||
})
|
||||
|
||||
// 3. Удаляем записи в audit_log связанные с пользователем
|
||||
await prisma.auditLog.deleteMany({
|
||||
where: { userId: id }
|
||||
})
|
||||
|
||||
// Теперь можно безопасно удалить пользователя
|
||||
await prisma.user.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.USER_DELETE,
|
||||
details: `${userToDelete.firstName} ${userToDelete.lastName} (${userToDelete.email})`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления пользователя:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить пользователя')
|
||||
}
|
||||
},
|
||||
|
||||
adminChangePassword: async (_: unknown, { input }: { input: AdminChangePasswordInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId || context.userRole !== 'ADMIN') {
|
||||
throw new Error('Недостаточно прав для выполнения операции')
|
||||
}
|
||||
|
||||
// Получаем данные пользователя
|
||||
const targetUser = await prisma.user.findUnique({ where: { id: input.userId } })
|
||||
if (!targetUser) {
|
||||
throw new Error('Пользователь не найден')
|
||||
}
|
||||
|
||||
// Хешируем новый пароль
|
||||
const hashedNewPassword = await hashPassword(input.newPassword)
|
||||
|
||||
// Обновляем пароль пользователя
|
||||
await prisma.user.update({
|
||||
where: { id: input.userId },
|
||||
data: { password: hashedNewPassword }
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PASSWORD_CHANGE,
|
||||
details: `Пароль пользователя ${targetUser.firstName} ${targetUser.lastName} (${targetUser.email})`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка смены пароля пользователя:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось сменить пароль пользователя')
|
||||
}
|
||||
},
|
||||
|
||||
// Категории
|
||||
createCategory: async (_: unknown, { input }: { input: CategoryInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const slug = input.slug || createSlug(input.name)
|
||||
|
||||
// Проверяем уникальность slug
|
||||
const existingCategory = await prisma.category.findUnique({
|
||||
where: { slug }
|
||||
})
|
||||
|
||||
if (existingCategory) {
|
||||
throw new Error('Категория с таким адресом уже существует')
|
||||
}
|
||||
|
||||
const category = await prisma.category.create({
|
||||
data: {
|
||||
...input,
|
||||
slug
|
||||
},
|
||||
include: {
|
||||
parent: true,
|
||||
children: true
|
||||
}
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.CATEGORY_CREATE,
|
||||
details: `Категория "${input.name}"`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return category
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания категории:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать категорию')
|
||||
}
|
||||
},
|
||||
|
||||
updateCategory: async (_: unknown, { id, input }: { id: string; input: CategoryInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = { ...input }
|
||||
|
||||
if (input.name && !input.slug) {
|
||||
updateData.slug = createSlug(input.name)
|
||||
}
|
||||
|
||||
const category = await prisma.category.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
parent: true,
|
||||
children: true
|
||||
}
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.CATEGORY_UPDATE,
|
||||
details: `Категория "${category.name}"`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return category
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления категории:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить категорию')
|
||||
}
|
||||
},
|
||||
|
||||
deleteCategory: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const category = await prisma.category.findUnique({
|
||||
where: { id },
|
||||
include: { children: true, products: true }
|
||||
})
|
||||
|
||||
if (!category) {
|
||||
throw new Error('Категория не найдена')
|
||||
}
|
||||
|
||||
if (category.children.length > 0) {
|
||||
throw new Error('Нельзя удалить категорию с подкатегориями')
|
||||
}
|
||||
|
||||
if (category.products.length > 0) {
|
||||
throw new Error('Нельзя удалить категорию с товарами')
|
||||
}
|
||||
|
||||
await prisma.category.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.CATEGORY_DELETE,
|
||||
details: `Категория "${category.name}"`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления категории:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить категорию')
|
||||
}
|
||||
},
|
||||
|
||||
// Товары
|
||||
createProduct: async (_: unknown, {
|
||||
input,
|
||||
images = [],
|
||||
characteristics = [],
|
||||
options = []
|
||||
}: {
|
||||
input: ProductInput;
|
||||
images?: ProductImageInput[];
|
||||
characteristics?: CharacteristicInput[];
|
||||
options?: ProductOptionInput[]
|
||||
}, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const slug = input.slug || createSlug(input.name)
|
||||
|
||||
// Проверяем уникальность slug
|
||||
const existingProduct = await prisma.product.findUnique({
|
||||
where: { slug }
|
||||
})
|
||||
|
||||
if (existingProduct) {
|
||||
throw new Error('Товар с таким адресом уже существует')
|
||||
}
|
||||
|
||||
// Проверяем уникальность артикула
|
||||
if (input.article) {
|
||||
const existingByArticle = await prisma.product.findUnique({
|
||||
where: { article: input.article }
|
||||
})
|
||||
|
||||
if (existingByArticle) {
|
||||
throw new Error('Товар с таким артикулом уже существует')
|
||||
}
|
||||
}
|
||||
|
||||
const { categoryIds, ...productData } = input
|
||||
|
||||
// Создаем товар
|
||||
const product = await prisma.product.create({
|
||||
data: {
|
||||
...productData,
|
||||
slug,
|
||||
categories: categoryIds ? {
|
||||
connect: categoryIds.map(id => ({ id }))
|
||||
} : undefined,
|
||||
images: {
|
||||
create: images.map((img, index) => ({
|
||||
...img,
|
||||
order: img.order ?? index
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Добавляем характеристики
|
||||
for (const char of characteristics) {
|
||||
let characteristic = await prisma.characteristic.findUnique({
|
||||
where: { name: char.name }
|
||||
})
|
||||
|
||||
if (!characteristic) {
|
||||
characteristic = await prisma.characteristic.create({
|
||||
data: { name: char.name }
|
||||
})
|
||||
}
|
||||
|
||||
await prisma.productCharacteristic.create({
|
||||
data: {
|
||||
productId: product.id,
|
||||
characteristicId: characteristic.id,
|
||||
value: char.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Добавляем опции
|
||||
for (const optionInput of options) {
|
||||
// Создаём или находим опцию
|
||||
let option = await prisma.option.findUnique({
|
||||
where: { name: optionInput.name }
|
||||
})
|
||||
|
||||
if (!option) {
|
||||
option = await prisma.option.create({
|
||||
data: {
|
||||
name: optionInput.name,
|
||||
type: optionInput.type
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Создаём значения опции и связываем с товаром
|
||||
for (const valueInput of optionInput.values) {
|
||||
// Создаём или находим значение опции
|
||||
let optionValue = await prisma.optionValue.findFirst({
|
||||
where: {
|
||||
optionId: option.id,
|
||||
value: valueInput.value
|
||||
}
|
||||
})
|
||||
|
||||
if (!optionValue) {
|
||||
optionValue = await prisma.optionValue.create({
|
||||
data: {
|
||||
optionId: option.id,
|
||||
value: valueInput.value,
|
||||
price: valueInput.price || 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Связываем товар с опцией и значением
|
||||
await prisma.productOption.create({
|
||||
data: {
|
||||
productId: product.id,
|
||||
optionId: option.id,
|
||||
optionValueId: optionValue.id
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем созданный товар со всеми связанными данными
|
||||
const createdProduct = await prisma.product.findUnique({
|
||||
where: { id: product.id },
|
||||
include: {
|
||||
categories: true,
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
options: {
|
||||
include: {
|
||||
option: { include: { values: true } },
|
||||
optionValue: true
|
||||
}
|
||||
},
|
||||
characteristics: { include: { characteristic: true } },
|
||||
relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } },
|
||||
accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } }
|
||||
}
|
||||
})
|
||||
|
||||
// Создаем запись в истории товара
|
||||
if (context.userId) {
|
||||
await prisma.productHistory.create({
|
||||
data: {
|
||||
productId: product.id,
|
||||
action: 'CREATE',
|
||||
changes: JSON.stringify({
|
||||
name: input.name,
|
||||
article: input.article,
|
||||
description: input.description,
|
||||
wholesalePrice: input.wholesalePrice,
|
||||
retailPrice: input.retailPrice,
|
||||
stock: input.stock,
|
||||
isVisible: input.isVisible,
|
||||
categories: categoryIds,
|
||||
images: images.length,
|
||||
characteristics: characteristics.length,
|
||||
options: options.length
|
||||
}),
|
||||
userId: context.userId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PRODUCT_CREATE,
|
||||
details: `Товар "${input.name}"`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return createdProduct
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания товара:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать товар')
|
||||
}
|
||||
},
|
||||
|
||||
updateProduct: async (_: unknown, {
|
||||
id,
|
||||
input,
|
||||
images = [],
|
||||
characteristics = [],
|
||||
options = []
|
||||
}: {
|
||||
id: string;
|
||||
input: ProductInput;
|
||||
images?: ProductImageInput[];
|
||||
characteristics?: CharacteristicInput[];
|
||||
options?: ProductOptionInput[]
|
||||
}, context: Context) => {
|
||||
try {
|
||||
console.log('updateProduct вызван с опциями:', JSON.stringify(options, null, 2))
|
||||
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const { categoryIds, ...productData } = input
|
||||
|
||||
// Удаляем старые изображения, характеристики и опции
|
||||
await prisma.productImage.deleteMany({
|
||||
where: { productId: id }
|
||||
})
|
||||
|
||||
await prisma.productCharacteristic.deleteMany({
|
||||
where: { productId: id }
|
||||
})
|
||||
|
||||
await prisma.productOption.deleteMany({
|
||||
where: { productId: id }
|
||||
})
|
||||
|
||||
// Обновляем основные данные товара
|
||||
const finalData = { ...productData }
|
||||
if (input.name && !input.slug) {
|
||||
finalData.slug = createSlug(input.name)
|
||||
}
|
||||
|
||||
await prisma.product.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...finalData,
|
||||
categories: categoryIds ? {
|
||||
set: categoryIds.map((catId: string) => ({ id: catId }))
|
||||
} : undefined,
|
||||
images: {
|
||||
create: images.map((img, index) => ({
|
||||
...img,
|
||||
order: img.order ?? index
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Добавляем новые характеристики
|
||||
for (const char of characteristics) {
|
||||
let characteristic = await prisma.characteristic.findUnique({
|
||||
where: { name: char.name }
|
||||
})
|
||||
|
||||
if (!characteristic) {
|
||||
characteristic = await prisma.characteristic.create({
|
||||
data: { name: char.name }
|
||||
})
|
||||
}
|
||||
|
||||
await prisma.productCharacteristic.create({
|
||||
data: {
|
||||
productId: id,
|
||||
characteristicId: characteristic.id,
|
||||
value: char.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Добавляем новые опции
|
||||
console.log(`Обрабатываем ${options.length} опций для товара ${id}`)
|
||||
for (const optionInput of options) {
|
||||
console.log('Обрабатываем опцию:', optionInput)
|
||||
|
||||
// Создаём или находим опцию
|
||||
let option = await prisma.option.findUnique({
|
||||
where: { name: optionInput.name }
|
||||
})
|
||||
|
||||
if (!option) {
|
||||
console.log('Создаем новую опцию:', optionInput.name)
|
||||
option = await prisma.option.create({
|
||||
data: {
|
||||
name: optionInput.name,
|
||||
type: optionInput.type
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log('Найдена существующая опция:', option.id)
|
||||
}
|
||||
|
||||
// Создаём значения опции и связываем с товаром
|
||||
for (const valueInput of optionInput.values) {
|
||||
console.log('Обрабатываем значение опции:', valueInput)
|
||||
|
||||
// Создаём или находим значение опции
|
||||
let optionValue = await prisma.optionValue.findFirst({
|
||||
where: {
|
||||
optionId: option.id,
|
||||
value: valueInput.value
|
||||
}
|
||||
})
|
||||
|
||||
if (!optionValue) {
|
||||
console.log('Создаем новое значение опции')
|
||||
optionValue = await prisma.optionValue.create({
|
||||
data: {
|
||||
optionId: option.id,
|
||||
value: valueInput.value,
|
||||
price: valueInput.price || 0
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log('Найдено существующее значение опции:', optionValue.id)
|
||||
}
|
||||
|
||||
// Связываем товар с опцией и значением
|
||||
console.log('Создаем связь товар-опция-значение')
|
||||
await prisma.productOption.create({
|
||||
data: {
|
||||
productId: id,
|
||||
optionId: option.id,
|
||||
optionValueId: optionValue.id
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log('Все опции обработаны')
|
||||
|
||||
// Получаем обновленный товар со всеми связанными данными
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
categories: true,
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
options: {
|
||||
include: {
|
||||
option: { include: { values: true } },
|
||||
optionValue: true
|
||||
}
|
||||
},
|
||||
characteristics: { include: { characteristic: true } },
|
||||
relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } },
|
||||
accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } }
|
||||
}
|
||||
})
|
||||
|
||||
// Создаем запись в истории товара
|
||||
if (context.userId && product) {
|
||||
await prisma.productHistory.create({
|
||||
data: {
|
||||
productId: id,
|
||||
action: 'UPDATE',
|
||||
changes: JSON.stringify({
|
||||
name: input.name,
|
||||
article: input.article,
|
||||
description: input.description,
|
||||
wholesalePrice: input.wholesalePrice,
|
||||
retailPrice: input.retailPrice,
|
||||
stock: input.stock,
|
||||
isVisible: input.isVisible,
|
||||
categories: categoryIds,
|
||||
images: images.length,
|
||||
characteristics: characteristics.length,
|
||||
options: options.length
|
||||
}),
|
||||
userId: context.userId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PRODUCT_UPDATE,
|
||||
details: `Товар "${product?.name}"`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return product
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления товара:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить товар')
|
||||
}
|
||||
},
|
||||
|
||||
deleteProduct: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
throw new Error('Товар не найден')
|
||||
}
|
||||
|
||||
await prisma.product.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PRODUCT_DELETE,
|
||||
details: `Товар "${product.name}"`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления товара:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить товар')
|
||||
}
|
||||
},
|
||||
|
||||
updateProductVisibility: async (_: unknown, { id, isVisible }: { id: string; isVisible: boolean }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const product = await prisma.product.update({
|
||||
where: { id },
|
||||
data: { isVisible },
|
||||
include: {
|
||||
categories: true,
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
options: {
|
||||
include: {
|
||||
option: { include: { values: true } },
|
||||
optionValue: true
|
||||
}
|
||||
},
|
||||
characteristics: { include: { characteristic: true } },
|
||||
relatedProducts: { include: { images: { orderBy: { order: 'asc' } } } },
|
||||
accessoryProducts: { include: { images: { orderBy: { order: 'asc' } } } }
|
||||
}
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PRODUCT_UPDATE,
|
||||
details: `Изменена видимость товара "${product.name}" на ${isVisible ? 'видимый' : 'скрытый'}`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return product
|
||||
} catch (error) {
|
||||
console.error('Ошибка изменения видимости товара:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось изменить видимость товара')
|
||||
}
|
||||
},
|
||||
|
||||
// Массовые операции с товарами
|
||||
deleteProducts: async (_: unknown, { ids }: { ids: string[] }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
if (!ids || ids.length === 0) {
|
||||
throw new Error('Не указаны товары для удаления')
|
||||
}
|
||||
|
||||
// Получаем информацию о товарах для логирования
|
||||
const products = await prisma.product.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: { id: true, name: true }
|
||||
})
|
||||
|
||||
// Удаляем товары
|
||||
const result = await prisma.product.deleteMany({
|
||||
where: { id: { in: ids } }
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PRODUCT_DELETE,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
details: `Массовое удаление товаров: ${products.map((p: any) => p.name).join(', ')} (${result.count} шт.)`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return { count: result.count }
|
||||
} catch (error) {
|
||||
console.error('Ошибка массового удаления товаров:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить товары')
|
||||
}
|
||||
},
|
||||
|
||||
updateProductsVisibility: async (_: unknown, { ids, isVisible }: { ids: string[]; isVisible: boolean }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
if (!ids || ids.length === 0) {
|
||||
throw new Error('Не указаны товары для изменения видимости')
|
||||
}
|
||||
|
||||
// Получаем информацию о товарах для логирования
|
||||
const products = await prisma.product.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: { id: true, name: true }
|
||||
})
|
||||
|
||||
// Обновляем видимость товаров
|
||||
const result = await prisma.product.updateMany({
|
||||
where: { id: { in: ids } },
|
||||
data: { isVisible }
|
||||
})
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PRODUCT_UPDATE,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
details: `Массовое изменение видимости товаров на ${isVisible ? 'видимые' : 'скрытые'}: ${products.map((p: any) => p.name).join(', ')} (${result.count} шт.)`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return { count: result.count }
|
||||
} catch (error) {
|
||||
console.error('Ошибка массового изменения видимости товаров:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось изменить видимость товаров')
|
||||
}
|
||||
},
|
||||
|
||||
exportProducts: async (_: unknown, { categoryId, search }: {
|
||||
categoryId?: string; search?: string; format?: string
|
||||
}, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Получаем товары с теми же фильтрами, что и в списке
|
||||
const where: Record<string, unknown> = {}
|
||||
|
||||
if (categoryId) {
|
||||
where.categories = { some: { id: categoryId } }
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ article: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
|
||||
const products = await prisma.product.findMany({
|
||||
where,
|
||||
include: {
|
||||
categories: true,
|
||||
images: { orderBy: { order: 'asc' } },
|
||||
characteristics: { include: { characteristic: true } },
|
||||
options: {
|
||||
include: {
|
||||
option: true,
|
||||
optionValue: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { name: 'asc' }
|
||||
})
|
||||
|
||||
// Создаем CSV данные
|
||||
const csvData = products.map(product => ({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
article: product.article || '',
|
||||
description: product.description || '',
|
||||
wholesalePrice: product.wholesalePrice || 0,
|
||||
retailPrice: product.retailPrice || 0,
|
||||
stock: product.stock,
|
||||
isVisible: product.isVisible ? 'Да' : 'Нет',
|
||||
weight: product.weight || 0,
|
||||
dimensions: product.dimensions || '',
|
||||
unit: product.unit,
|
||||
categories: product.categories.map(cat => cat.name).join(', '),
|
||||
images: product.images.map(img => img.url).join(', '),
|
||||
characteristics: product.characteristics.map(char =>
|
||||
`${char.characteristic.name}: ${char.value}`
|
||||
).join('; '),
|
||||
options: product.options.map(opt =>
|
||||
`${opt.option.name}: ${opt.optionValue.value} (+${opt.optionValue.price}₽)`
|
||||
).join('; '),
|
||||
videoUrl: product.videoUrl || '',
|
||||
createdAt: product.createdAt instanceof Date ? product.createdAt.toISOString() : product.createdAt,
|
||||
updatedAt: product.updatedAt instanceof Date ? product.updatedAt.toISOString() : product.updatedAt
|
||||
}))
|
||||
|
||||
// Создаем CSV строку
|
||||
const createCsvWriter = csvWriter.createObjectCsvStringifier({
|
||||
header: [
|
||||
{ id: 'id', title: 'ID' },
|
||||
{ id: 'name', title: 'Название' },
|
||||
{ id: 'article', title: 'Артикул' },
|
||||
{ id: 'description', title: 'Описание' },
|
||||
{ id: 'wholesalePrice', title: 'Цена опт' },
|
||||
{ id: 'retailPrice', title: 'Цена розница' },
|
||||
{ id: 'stock', title: 'Остаток' },
|
||||
{ id: 'isVisible', title: 'Видимый' },
|
||||
{ id: 'weight', title: 'Вес' },
|
||||
{ id: 'dimensions', title: 'Размеры' },
|
||||
{ id: 'unit', title: 'Единица' },
|
||||
{ id: 'categories', title: 'Категории' },
|
||||
{ id: 'images', title: 'Изображения' },
|
||||
{ id: 'characteristics', title: 'Характеристики' },
|
||||
{ id: 'options', title: 'Опции' },
|
||||
{ id: 'videoUrl', title: 'Видео' },
|
||||
{ id: 'createdAt', title: 'Создан' },
|
||||
{ id: 'updatedAt', title: 'Обновлен' }
|
||||
]
|
||||
})
|
||||
|
||||
const csvString = createCsvWriter.getHeaderString() + createCsvWriter.stringifyRecords(csvData)
|
||||
const csvBuffer = Buffer.from(csvString, 'utf8')
|
||||
|
||||
// Генерируем имя файла
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')
|
||||
const filename = `products-export-${timestamp}.csv`
|
||||
const key = generateFileKey(filename, 'exports')
|
||||
|
||||
// Загружаем в S3
|
||||
const uploadResult = await uploadBuffer(csvBuffer, key, 'text/csv')
|
||||
|
||||
// Логируем действие
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PRODUCT_UPDATE, // Можно добавить новый тип EXPORT
|
||||
details: `Экспорт товаров: ${products.length} шт. (${categoryId ? 'категория' : 'все'})`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
url: uploadResult.url,
|
||||
filename,
|
||||
count: products.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка экспорта товаров:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось экспортировать товары')
|
||||
}
|
||||
},
|
||||
|
||||
importProducts: async (_: unknown, { input }: {
|
||||
input: { file: string; categoryId?: string; replaceExisting?: boolean }
|
||||
}, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Декодируем base64 файл
|
||||
console.log('Начало импорта товаров, пользователь:', context.userId)
|
||||
const fileData = Buffer.from(input.file, 'base64')
|
||||
console.log('Размер файла:', fileData.length, 'байт')
|
||||
|
||||
let headers: string[] = []
|
||||
let dataRows: string[][] = []
|
||||
|
||||
// Определяем тип файла по содержимому и размеру
|
||||
const hasExcelSignature = (fileData[0] === 0x50 && fileData[1] === 0x4B) || // PK (Excel/ZIP signature)
|
||||
(fileData[0] === 0xD0 && fileData[1] === 0xCF) // OLE signature (старые Excel файлы)
|
||||
|
||||
// Дополнительно проверяем размер файла (Excel файлы обычно больше 1KB)
|
||||
const isExcel = hasExcelSignature && fileData.length > 1024
|
||||
|
||||
if (isExcel) {
|
||||
try {
|
||||
// Парсим Excel файл
|
||||
console.log('Парсим Excel файл, размер:', fileData.length, 'байт')
|
||||
const workbook = XLSX.read(fileData, { type: 'buffer' })
|
||||
|
||||
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
|
||||
throw new Error('Excel файл не содержит листов с данными')
|
||||
}
|
||||
|
||||
const sheetName = workbook.SheetNames[0]
|
||||
const worksheet = workbook.Sheets[sheetName]
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as string[][]
|
||||
|
||||
console.log('Обработано строк из Excel:', jsonData.length)
|
||||
|
||||
if (jsonData.length < 2) {
|
||||
throw new Error('Файл должен содержать заголовки и хотя бы одну строку данных')
|
||||
}
|
||||
|
||||
headers = jsonData[0].map(h => String(h || '').trim())
|
||||
dataRows = jsonData.slice(1).filter(row => row.some(cell => String(cell || '').trim()))
|
||||
|
||||
console.log('Заголовки:', headers)
|
||||
console.log('Строк данных:', dataRows.length)
|
||||
} catch (excelError) {
|
||||
console.error('Ошибка парсинга Excel файла:', excelError)
|
||||
throw new Error('Не удалось прочитать Excel файл. Убедитесь, что файл не поврежден.')
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// Парсим как CSV файл
|
||||
console.log('Парсим как CSV файл, размер:', fileData.length, 'байт')
|
||||
const fileContent = fileData.toString('utf-8')
|
||||
const lines = fileContent.split('\n').filter(line => line.trim())
|
||||
|
||||
console.log('Строк в CSV:', lines.length)
|
||||
|
||||
if (lines.length < 2) {
|
||||
throw new Error('Файл должен содержать заголовки и хотя бы одну строку данных')
|
||||
}
|
||||
|
||||
headers = lines[0].split(',').map(h => h.replace(/"/g, '').trim())
|
||||
dataRows = lines.slice(1).map(line =>
|
||||
line.split(',').map(v => v.replace(/"/g, '').trim())
|
||||
)
|
||||
|
||||
console.log('Заголовки CSV:', headers)
|
||||
console.log('Строк данных CSV:', dataRows.length)
|
||||
} catch (csvError) {
|
||||
console.error('Ошибка парсинга CSV файла:', csvError)
|
||||
throw new Error('Не удалось прочитать файл. Поддерживаются только форматы .xlsx и .csv')
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: 0,
|
||||
errors: [] as string[],
|
||||
total: dataRows.length,
|
||||
warnings: [] as string[]
|
||||
}
|
||||
|
||||
// Обрабатываем каждую строку
|
||||
for (let i = 0; i < dataRows.length; i++) {
|
||||
const lineNumber = i + 2
|
||||
|
||||
try {
|
||||
const values = dataRows[i].map(v => String(v || '').trim())
|
||||
|
||||
// Если строка содержит меньше колонок, дополняем пустыми значениями
|
||||
// Если больше - обрезаем до нужного количества
|
||||
while (values.length < headers.length) {
|
||||
values.push('')
|
||||
}
|
||||
if (values.length > headers.length) {
|
||||
values.splice(headers.length)
|
||||
}
|
||||
|
||||
// Создаем объект из заголовков и значений
|
||||
const rowData: Record<string, string> = {}
|
||||
headers.forEach((header, index) => {
|
||||
rowData[header] = values[index]
|
||||
})
|
||||
|
||||
// Валидация обязательных полей
|
||||
const name = rowData['Название'] || rowData['Наименование'] || rowData['name'] || ''
|
||||
if (!name) {
|
||||
result.errors.push(`Строка ${lineNumber}: отсутствует название товара`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Проверяем существование товара по артикулу
|
||||
const article = rowData['Артикул'] || rowData['article'] || ''
|
||||
let existingProduct: any = null
|
||||
|
||||
if (article) {
|
||||
existingProduct = await prisma.product.findFirst({
|
||||
where: { article }
|
||||
})
|
||||
}
|
||||
|
||||
// Если товар существует и не включен режим замещения
|
||||
if (existingProduct && !input.replaceExisting) {
|
||||
result.warnings.push(`Строка ${lineNumber}: товар с артикулом "${article}" уже существует`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Подготовка данных для создания/обновления товара
|
||||
const manufacturer = rowData['Производитель'] || rowData['manufacturer'] || ''
|
||||
const description = rowData['Описание'] || rowData['description'] ||
|
||||
(manufacturer ? `Производитель: ${manufacturer}` : undefined)
|
||||
|
||||
const productData = {
|
||||
name: name,
|
||||
article: article || undefined,
|
||||
description: description,
|
||||
wholesalePrice: parseFloat(rowData['Цена опт'] || rowData['wholesalePrice'] || rowData['Цена АвтоЕвро ООО НДС'] || '0') || undefined,
|
||||
retailPrice: parseFloat(rowData['Цена розница'] || rowData['retailPrice'] || rowData['Цена АвтоЕвро ООО НДС'] || '0') || undefined,
|
||||
stock: parseInt(rowData['Остаток'] || rowData['Доступно'] || rowData['stock'] || '0') || 0,
|
||||
unit: rowData['Единица'] || rowData['unit'] || 'шт',
|
||||
weight: parseFloat(rowData['Вес'] || rowData['weight'] || '0') || undefined,
|
||||
dimensions: rowData['Размеры'] || rowData['dimensions'] || undefined,
|
||||
isVisible: true
|
||||
}
|
||||
|
||||
// Генерируем slug
|
||||
const slug = createSlug(productData.name)
|
||||
|
||||
if (existingProduct && input.replaceExisting) {
|
||||
// Обновляем существующий товар
|
||||
await prisma.product.update({
|
||||
where: { id: existingProduct.id },
|
||||
data: {
|
||||
...productData,
|
||||
slug,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Создаем новый товар
|
||||
const createData: any = {
|
||||
...productData,
|
||||
slug
|
||||
}
|
||||
|
||||
// Добавляем категорию если указана
|
||||
if (input.categoryId) {
|
||||
createData.categories = {
|
||||
connect: [{ id: input.categoryId }]
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.product.create({
|
||||
data: createData
|
||||
})
|
||||
}
|
||||
|
||||
result.success++
|
||||
} catch (error) {
|
||||
console.error(`Ошибка обработки строки ${lineNumber}:`, error)
|
||||
result.errors.push(`Строка ${lineNumber}: ошибка создания товара`)
|
||||
}
|
||||
}
|
||||
|
||||
// Логируем действие
|
||||
console.log('Результат импорта:', {
|
||||
total: result.total,
|
||||
success: result.success,
|
||||
errors: result.errors.length,
|
||||
warnings: result.warnings.length
|
||||
})
|
||||
|
||||
if (context.headers) {
|
||||
const { ipAddress, userAgent } = getClientInfo(context.headers)
|
||||
await createAuditLog({
|
||||
userId: context.userId,
|
||||
action: AuditAction.PRODUCT_UPDATE,
|
||||
details: `Импорт товаров: ${result.success} успешно, ${result.errors.length} ошибок`,
|
||||
ipAddress,
|
||||
userAgent
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Ошибка импорта товаров:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось импортировать товары')
|
||||
}
|
||||
},
|
||||
|
||||
// Опции
|
||||
createOption: async (_: unknown, { input }: { input: OptionInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const option = await prisma.option.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
values: {
|
||||
create: input.values.map(value => ({
|
||||
value: value.value,
|
||||
price: value.price || 0
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: { values: true }
|
||||
})
|
||||
|
||||
return option
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания опции:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать опцию')
|
||||
}
|
||||
},
|
||||
|
||||
updateOption: async (_: unknown, { id, input }: { id: string; input: OptionInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Удаляем старые значения
|
||||
await prisma.optionValue.deleteMany({
|
||||
where: { optionId: id }
|
||||
})
|
||||
|
||||
const option = await prisma.option.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
values: {
|
||||
create: input.values.map(value => ({
|
||||
value: value.value,
|
||||
price: value.price || 0
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: { values: true }
|
||||
})
|
||||
|
||||
return option
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления опции:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить опцию')
|
||||
}
|
||||
},
|
||||
|
||||
deleteOption: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
await prisma.option.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления опции:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить опцию')
|
||||
}
|
||||
},
|
||||
|
||||
// Клиенты
|
||||
createClient: async (_: unknown, { input, vehicles = [], discounts = [] }: {
|
||||
input: ClientInput; vehicles?: ClientVehicleInput[]; discounts?: ClientDiscountInput[]
|
||||
}, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Генерируем номер клиента, если не указан
|
||||
let clientNumber = input.clientNumber
|
||||
if (!clientNumber) {
|
||||
const lastClient = await prisma.client.findFirst({
|
||||
orderBy: { clientNumber: 'desc' }
|
||||
})
|
||||
const lastNumber = lastClient ? parseInt(lastClient.clientNumber) : 100000
|
||||
clientNumber = (lastNumber + 1).toString()
|
||||
}
|
||||
|
||||
const client = await prisma.client.create({
|
||||
data: {
|
||||
clientNumber,
|
||||
type: input.type,
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
phone: input.phone,
|
||||
city: input.city,
|
||||
markup: input.markup,
|
||||
isConfirmed: input.isConfirmed ?? false,
|
||||
profileId: input.profileId,
|
||||
legalEntityType: input.legalEntityType,
|
||||
inn: input.inn,
|
||||
kpp: input.kpp,
|
||||
ogrn: input.ogrn,
|
||||
okpo: input.okpo,
|
||||
legalAddress: input.legalAddress,
|
||||
actualAddress: input.actualAddress,
|
||||
bankAccount: input.bankAccount,
|
||||
bankName: input.bankName,
|
||||
bankBik: input.bankBik,
|
||||
correspondentAccount: input.correspondentAccount,
|
||||
vehicles: {
|
||||
create: vehicles
|
||||
},
|
||||
discounts: {
|
||||
create: discounts
|
||||
}
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
vehicles: true,
|
||||
discounts: true
|
||||
}
|
||||
})
|
||||
|
||||
return client
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать клиента')
|
||||
}
|
||||
},
|
||||
|
||||
updateClient: async (_: unknown, { id, input, vehicles = [], discounts = [] }: {
|
||||
id: string; input: ClientInput; vehicles?: ClientVehicleInput[]; discounts?: ClientDiscountInput[]
|
||||
}, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Удаляем старые связанные данные
|
||||
await prisma.clientVehicle.deleteMany({ where: { clientId: id } })
|
||||
await prisma.clientDiscount.deleteMany({ where: { clientId: id } })
|
||||
|
||||
const client = await prisma.client.update({
|
||||
where: { id },
|
||||
data: {
|
||||
type: input.type,
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
phone: input.phone,
|
||||
city: input.city,
|
||||
markup: input.markup,
|
||||
isConfirmed: input.isConfirmed,
|
||||
profileId: input.profileId,
|
||||
legalEntityType: input.legalEntityType,
|
||||
inn: input.inn,
|
||||
kpp: input.kpp,
|
||||
ogrn: input.ogrn,
|
||||
okpo: input.okpo,
|
||||
legalAddress: input.legalAddress,
|
||||
actualAddress: input.actualAddress,
|
||||
bankAccount: input.bankAccount,
|
||||
bankName: input.bankName,
|
||||
bankBik: input.bankBik,
|
||||
correspondentAccount: input.correspondentAccount,
|
||||
vehicles: {
|
||||
create: vehicles
|
||||
},
|
||||
discounts: {
|
||||
create: discounts
|
||||
}
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
vehicles: true,
|
||||
discounts: true
|
||||
}
|
||||
})
|
||||
|
||||
return client
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить клиента')
|
||||
}
|
||||
},
|
||||
|
||||
deleteClient: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
await prisma.client.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить клиента')
|
||||
}
|
||||
},
|
||||
|
||||
confirmClient: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const client = await prisma.client.update({
|
||||
where: { id },
|
||||
data: { isConfirmed: true },
|
||||
include: {
|
||||
profile: true,
|
||||
vehicles: true,
|
||||
discounts: true
|
||||
}
|
||||
})
|
||||
|
||||
return client
|
||||
} catch (error) {
|
||||
console.error('Ошибка подтверждения клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось подтвердить клиента')
|
||||
}
|
||||
},
|
||||
|
||||
exportClients: async (_: unknown, { filter, search }: {
|
||||
filter?: ClientFilterInput; search?: string
|
||||
}, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = {}
|
||||
|
||||
if (filter) {
|
||||
if (filter.type) {
|
||||
where.type = filter.type
|
||||
}
|
||||
if (filter.registeredFrom || filter.registeredTo) {
|
||||
where.createdAt = {}
|
||||
if (filter.registeredFrom) {
|
||||
(where.createdAt as Record<string, unknown>).gte = filter.registeredFrom
|
||||
}
|
||||
if (filter.registeredTo) {
|
||||
(where.createdAt as Record<string, unknown>).lte = filter.registeredTo
|
||||
}
|
||||
}
|
||||
if (filter.unconfirmed) {
|
||||
where.isConfirmed = false
|
||||
}
|
||||
if (filter.profileId) {
|
||||
where.profileId = filter.profileId
|
||||
}
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ email: { contains: search, mode: 'insensitive' } },
|
||||
{ phone: { contains: search, mode: 'insensitive' } },
|
||||
{ clientNumber: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
|
||||
const clients = await prisma.client.findMany({
|
||||
where,
|
||||
include: {
|
||||
profile: true,
|
||||
vehicles: true,
|
||||
discounts: true
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
// Создаем CSV данные
|
||||
const csvData = clients.map(client => ({
|
||||
id: client.id,
|
||||
clientNumber: client.clientNumber,
|
||||
type: client.type === 'INDIVIDUAL' ? 'Физ. лицо' : 'Юр. лицо',
|
||||
name: client.name,
|
||||
email: client.email || '',
|
||||
phone: client.phone,
|
||||
city: client.city || '',
|
||||
markup: client.markup || 0,
|
||||
isConfirmed: client.isConfirmed ? 'Да' : 'Нет',
|
||||
profile: client.profile?.name || '',
|
||||
legalEntityType: client.legalEntityType || '',
|
||||
inn: client.inn || '',
|
||||
kpp: client.kpp || '',
|
||||
ogrn: client.ogrn || '',
|
||||
okpo: client.okpo || '',
|
||||
legalAddress: client.legalAddress || '',
|
||||
actualAddress: client.actualAddress || '',
|
||||
bankAccount: client.bankAccount || '',
|
||||
bankName: client.bankName || '',
|
||||
bankBik: client.bankBik || '',
|
||||
correspondentAccount: client.correspondentAccount || '',
|
||||
vehicles: client.vehicles.map(v =>
|
||||
`${v.brand || ''} ${v.model || ''} (${v.licensePlate || v.vin || v.frame || ''})`
|
||||
).join('; '),
|
||||
createdAt: client.createdAt instanceof Date ? client.createdAt.toISOString() : client.createdAt
|
||||
}))
|
||||
|
||||
// Создаем CSV строку
|
||||
const createCsvWriter = csvWriter.createObjectCsvStringifier({
|
||||
header: [
|
||||
{ id: 'clientNumber', title: 'Номер клиента' },
|
||||
{ id: 'type', title: 'Тип' },
|
||||
{ id: 'name', title: 'Имя' },
|
||||
{ id: 'email', title: 'Email' },
|
||||
{ id: 'phone', title: 'Телефон' },
|
||||
{ id: 'city', title: 'Город' },
|
||||
{ id: 'markup', title: 'Наценка' },
|
||||
{ id: 'isConfirmed', title: 'Подтвержден' },
|
||||
{ id: 'profile', title: 'Профиль' },
|
||||
{ id: 'legalEntityType', title: 'Тип юр. лица' },
|
||||
{ id: 'inn', title: 'ИНН' },
|
||||
{ id: 'kpp', title: 'КПП' },
|
||||
{ id: 'ogrn', title: 'ОГРН' },
|
||||
{ id: 'okpo', title: 'ОКПО' },
|
||||
{ id: 'legalAddress', title: 'Юридический адрес' },
|
||||
{ id: 'actualAddress', title: 'Фактический адрес' },
|
||||
{ id: 'bankAccount', title: 'Расчетный счет' },
|
||||
{ id: 'bankName', title: 'Банк' },
|
||||
{ id: 'bankBik', title: 'БИК' },
|
||||
{ id: 'correspondentAccount', title: 'Корр. счет' },
|
||||
{ id: 'vehicles', title: 'Автомобили' },
|
||||
{ id: 'createdAt', title: 'Дата регистрации' }
|
||||
]
|
||||
})
|
||||
|
||||
const csvString = createCsvWriter.getHeaderString() + createCsvWriter.stringifyRecords(csvData)
|
||||
const csvBuffer = Buffer.from(csvString, 'utf8')
|
||||
|
||||
// Генерируем имя файла
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')
|
||||
const filename = `clients-export-${timestamp}.csv`
|
||||
const key = generateFileKey(filename, 'exports')
|
||||
|
||||
// Загружаем в S3
|
||||
const uploadResult = await uploadBuffer(csvBuffer, key, 'text/csv')
|
||||
|
||||
return {
|
||||
url: uploadResult.url,
|
||||
filename,
|
||||
count: clients.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка экспорта клиентов:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось экспортировать клиентов')
|
||||
}
|
||||
},
|
||||
|
||||
// Профили клиентов
|
||||
createClientProfile: async (_: unknown, { input }: { input: ClientProfileInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Генерируем код профиля, если не указан
|
||||
let code = input.code
|
||||
if (!code) {
|
||||
const lastProfile = await prisma.clientProfile.findFirst({
|
||||
orderBy: { code: 'desc' }
|
||||
})
|
||||
const lastNumber = lastProfile ? parseInt(lastProfile.code) : 1000000
|
||||
code = (lastNumber + 1).toString()
|
||||
}
|
||||
|
||||
const profile = await prisma.clientProfile.create({
|
||||
data: {
|
||||
code,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
baseMarkup: input.baseMarkup,
|
||||
autoSendInvoice: input.autoSendInvoice ?? true,
|
||||
vinRequestModule: input.vinRequestModule ?? false,
|
||||
priceRangeMarkups: {
|
||||
create: input.priceRangeMarkups || []
|
||||
},
|
||||
orderDiscounts: {
|
||||
create: input.orderDiscounts || []
|
||||
},
|
||||
supplierMarkups: {
|
||||
create: input.supplierMarkups || []
|
||||
},
|
||||
brandMarkups: {
|
||||
create: input.brandMarkups || []
|
||||
},
|
||||
categoryMarkups: {
|
||||
create: input.categoryMarkups || []
|
||||
},
|
||||
excludedBrands: {
|
||||
create: (input.excludedBrands || []).map(brandName => ({ brandName }))
|
||||
},
|
||||
excludedCategories: {
|
||||
create: (input.excludedCategories || []).map(categoryName => ({ categoryName }))
|
||||
},
|
||||
paymentTypes: {
|
||||
create: input.paymentTypes || []
|
||||
}
|
||||
},
|
||||
include: {
|
||||
priceRangeMarkups: true,
|
||||
orderDiscounts: true,
|
||||
supplierMarkups: true,
|
||||
brandMarkups: true,
|
||||
categoryMarkups: true,
|
||||
excludedBrands: true,
|
||||
excludedCategories: true,
|
||||
paymentTypes: true
|
||||
}
|
||||
})
|
||||
|
||||
return profile
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания профиля клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать профиль клиента')
|
||||
}
|
||||
},
|
||||
|
||||
updateClientProfile: async (_: unknown, { id, input }: { id: string; input: ClientProfileInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Удаляем старые связанные данные
|
||||
await prisma.profilePriceRangeMarkup.deleteMany({ where: { profileId: id } })
|
||||
await prisma.profileOrderDiscount.deleteMany({ where: { profileId: id } })
|
||||
await prisma.profileSupplierMarkup.deleteMany({ where: { profileId: id } })
|
||||
await prisma.profileBrandMarkup.deleteMany({ where: { profileId: id } })
|
||||
await prisma.profileCategoryMarkup.deleteMany({ where: { profileId: id } })
|
||||
await prisma.profileExcludedBrand.deleteMany({ where: { profileId: id } })
|
||||
await prisma.profileExcludedCategory.deleteMany({ where: { profileId: id } })
|
||||
await prisma.profilePaymentType.deleteMany({ where: { profileId: id } })
|
||||
|
||||
const profile = await prisma.clientProfile.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
baseMarkup: input.baseMarkup,
|
||||
autoSendInvoice: input.autoSendInvoice,
|
||||
vinRequestModule: input.vinRequestModule,
|
||||
priceRangeMarkups: {
|
||||
create: input.priceRangeMarkups || []
|
||||
},
|
||||
orderDiscounts: {
|
||||
create: input.orderDiscounts || []
|
||||
},
|
||||
supplierMarkups: {
|
||||
create: input.supplierMarkups || []
|
||||
},
|
||||
brandMarkups: {
|
||||
create: input.brandMarkups || []
|
||||
},
|
||||
categoryMarkups: {
|
||||
create: input.categoryMarkups || []
|
||||
},
|
||||
excludedBrands: {
|
||||
create: (input.excludedBrands || []).map(brandName => ({ brandName }))
|
||||
},
|
||||
excludedCategories: {
|
||||
create: (input.excludedCategories || []).map(categoryName => ({ categoryName }))
|
||||
},
|
||||
paymentTypes: {
|
||||
create: input.paymentTypes || []
|
||||
}
|
||||
},
|
||||
include: {
|
||||
priceRangeMarkups: true,
|
||||
orderDiscounts: true,
|
||||
supplierMarkups: true,
|
||||
brandMarkups: true,
|
||||
categoryMarkups: true,
|
||||
excludedBrands: true,
|
||||
excludedCategories: true,
|
||||
paymentTypes: true
|
||||
}
|
||||
})
|
||||
|
||||
return profile
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления профиля клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить профиль клиента')
|
||||
}
|
||||
},
|
||||
|
||||
deleteClientProfile: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
await prisma.clientProfile.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления профиля клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить профиль клиента')
|
||||
}
|
||||
},
|
||||
|
||||
// Статусы клиентов
|
||||
createClientStatus: async (_: unknown, { input }: { input: ClientStatusInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const status = await prisma.clientStatus.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
color: input.color || '#6B7280',
|
||||
description: input.description
|
||||
}
|
||||
})
|
||||
|
||||
return status
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания статуса клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать статус клиента')
|
||||
}
|
||||
},
|
||||
|
||||
updateClientStatus: async (_: unknown, { id, input }: { id: string; input: ClientStatusInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const status = await prisma.clientStatus.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name,
|
||||
color: input.color,
|
||||
description: input.description
|
||||
}
|
||||
})
|
||||
|
||||
return status
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления статуса клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить статус клиента')
|
||||
}
|
||||
},
|
||||
|
||||
deleteClientStatus: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
await prisma.clientStatus.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления статуса клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить статус клиента')
|
||||
}
|
||||
},
|
||||
|
||||
// Скидки и промокоды
|
||||
createDiscount: async (_: unknown, { input }: { input: DiscountInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const discount = await prisma.discount.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
code: input.code,
|
||||
minOrderAmount: input.minOrderAmount || 0,
|
||||
discountType: input.discountType,
|
||||
discountValue: input.discountValue,
|
||||
isActive: input.isActive ?? true,
|
||||
validFrom: input.validFrom,
|
||||
validTo: input.validTo,
|
||||
profiles: {
|
||||
create: (input.profileIds || []).map(profileId => ({ profileId }))
|
||||
}
|
||||
},
|
||||
include: {
|
||||
profiles: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return discount
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания скидки:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать скидку')
|
||||
}
|
||||
},
|
||||
|
||||
updateDiscount: async (_: unknown, { id, input }: { id: string; input: DiscountInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Удаляем старые связи с профилями
|
||||
await prisma.discountProfile.deleteMany({ where: { discountId: id } })
|
||||
|
||||
const discount = await prisma.discount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
code: input.code,
|
||||
minOrderAmount: input.minOrderAmount,
|
||||
discountType: input.discountType,
|
||||
discountValue: input.discountValue,
|
||||
isActive: input.isActive,
|
||||
validFrom: input.validFrom,
|
||||
validTo: input.validTo,
|
||||
profiles: {
|
||||
create: (input.profileIds || []).map(profileId => ({ profileId }))
|
||||
}
|
||||
},
|
||||
include: {
|
||||
profiles: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return discount
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления скидки:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить скидку')
|
||||
}
|
||||
},
|
||||
|
||||
deleteDiscount: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
await prisma.discount.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления скидки:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить скидку')
|
||||
}
|
||||
},
|
||||
|
||||
// Обновление баланса клиента
|
||||
updateClientBalance: async (_: unknown, { id, newBalance, comment }: { id: string; newBalance: number; comment?: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const client = await prisma.client.findUnique({ where: { id } })
|
||||
if (!client) {
|
||||
throw new Error('Клиент не найден')
|
||||
}
|
||||
|
||||
// Создаем запись в истории изменений баланса
|
||||
await prisma.clientBalanceHistory.create({
|
||||
data: {
|
||||
clientId: id,
|
||||
userId: context.userId,
|
||||
oldValue: client.balance,
|
||||
newValue: newBalance,
|
||||
comment
|
||||
}
|
||||
})
|
||||
|
||||
// Обновляем баланс клиента
|
||||
const updatedClient = await prisma.client.update({
|
||||
where: { id },
|
||||
data: { balance: newBalance },
|
||||
include: {
|
||||
profile: true,
|
||||
manager: true,
|
||||
vehicles: true,
|
||||
discounts: true,
|
||||
deliveryAddresses: true,
|
||||
contacts: true,
|
||||
contracts: true,
|
||||
legalEntities: {
|
||||
include: {
|
||||
bankDetails: true
|
||||
}
|
||||
},
|
||||
bankDetails: {
|
||||
include: {
|
||||
legalEntity: true
|
||||
}
|
||||
},
|
||||
balanceHistory: {
|
||||
include: {
|
||||
user: true
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return updatedClient
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления баланса клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить баланс клиента')
|
||||
}
|
||||
},
|
||||
|
||||
// Транспорт клиента
|
||||
createClientVehicle: async (_: unknown, { clientId, input }: { clientId: string; input: ClientVehicleInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const vehicle = await prisma.clientVehicle.create({
|
||||
data: {
|
||||
clientId,
|
||||
name: input.name,
|
||||
vin: input.vin,
|
||||
frame: input.frame,
|
||||
licensePlate: input.licensePlate,
|
||||
brand: input.brand,
|
||||
model: input.model,
|
||||
modification: input.modification,
|
||||
year: input.year,
|
||||
mileage: input.mileage,
|
||||
comment: input.comment
|
||||
}
|
||||
})
|
||||
|
||||
return vehicle
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания транспорта:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать транспорт')
|
||||
}
|
||||
},
|
||||
|
||||
updateClientVehicle: async (_: unknown, { id, input }: { id: string; input: ClientVehicleInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const vehicle = await prisma.clientVehicle.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name,
|
||||
vin: input.vin,
|
||||
frame: input.frame,
|
||||
licensePlate: input.licensePlate,
|
||||
brand: input.brand,
|
||||
model: input.model,
|
||||
modification: input.modification,
|
||||
year: input.year,
|
||||
mileage: input.mileage,
|
||||
comment: input.comment
|
||||
}
|
||||
})
|
||||
|
||||
return vehicle
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления транспорта:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить транспорт')
|
||||
}
|
||||
},
|
||||
|
||||
deleteClientVehicle: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
await prisma.clientVehicle.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления транспорта:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить транспорт')
|
||||
}
|
||||
},
|
||||
|
||||
// Адреса доставки
|
||||
createClientDeliveryAddress: async (_: unknown, { clientId, input }: { clientId: string; input: ClientDeliveryAddressInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const address = await prisma.clientDeliveryAddress.create({
|
||||
data: {
|
||||
clientId,
|
||||
name: input.name,
|
||||
address: input.address,
|
||||
deliveryType: input.deliveryType,
|
||||
comment: input.comment
|
||||
}
|
||||
})
|
||||
|
||||
return address
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания адреса доставки:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать адрес доставки')
|
||||
}
|
||||
},
|
||||
|
||||
updateClientDeliveryAddress: async (_: unknown, { id, input }: { id: string; input: ClientDeliveryAddressInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const address = await prisma.clientDeliveryAddress.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name,
|
||||
address: input.address,
|
||||
deliveryType: input.deliveryType,
|
||||
comment: input.comment,
|
||||
// Дополнительные поля для курьерской доставки
|
||||
entrance: input.entrance,
|
||||
floor: input.floor,
|
||||
apartment: input.apartment,
|
||||
intercom: input.intercom,
|
||||
deliveryTime: input.deliveryTime,
|
||||
contactPhone: input.contactPhone
|
||||
}
|
||||
})
|
||||
|
||||
return address
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления адреса доставки:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить адрес доставки')
|
||||
}
|
||||
},
|
||||
|
||||
deleteClientDeliveryAddress: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
await prisma.clientDeliveryAddress.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления адреса доставки:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить адрес доставки')
|
||||
}
|
||||
},
|
||||
|
||||
// Контакты клиента
|
||||
createClientContact: async (_: unknown, { clientId, input }: { clientId: string; input: ClientContactInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const contact = await prisma.clientContact.create({
|
||||
data: {
|
||||
clientId,
|
||||
phone: input.phone,
|
||||
email: input.email,
|
||||
comment: input.comment
|
||||
}
|
||||
})
|
||||
|
||||
return contact
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания контакта:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать контакт')
|
||||
}
|
||||
},
|
||||
|
||||
updateClientContact: async (_: unknown, { id, input }: { id: string; input: ClientContactInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const contact = await prisma.clientContact.update({
|
||||
where: { id },
|
||||
data: {
|
||||
phone: input.phone,
|
||||
email: input.email,
|
||||
comment: input.comment
|
||||
}
|
||||
})
|
||||
|
||||
return contact
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления контакта:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить контакт')
|
||||
}
|
||||
},
|
||||
|
||||
deleteClientContact: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
await prisma.clientContact.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления контакта:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить контакт')
|
||||
}
|
||||
},
|
||||
|
||||
// Договоры
|
||||
createClientContract: async (_: unknown, { clientId, input }: { clientId: string; input: ClientContractInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const contract = await prisma.clientContract.create({
|
||||
data: {
|
||||
clientId,
|
||||
contractNumber: input.contractNumber,
|
||||
contractDate: input.contractDate || new Date(),
|
||||
name: input.name,
|
||||
ourLegalEntity: input.ourLegalEntity || '',
|
||||
clientLegalEntity: input.clientLegalEntity || '',
|
||||
balance: input.balance || 0,
|
||||
currency: input.currency || 'RUB',
|
||||
isActive: input.isActive ?? true,
|
||||
isDefault: input.isDefault ?? false,
|
||||
contractType: input.contractType || 'STANDARD',
|
||||
relationship: input.relationship || 'DIRECT',
|
||||
paymentDelay: input.paymentDelay ?? false,
|
||||
creditLimit: input.creditLimit,
|
||||
delayDays: input.delayDays,
|
||||
fileUrl: input.fileUrl
|
||||
}
|
||||
})
|
||||
|
||||
return contract
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания договора:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать договор')
|
||||
}
|
||||
},
|
||||
|
||||
updateClientContract: async (_: unknown, { id, input }: { id: string; input: ClientContractInput }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
const contract = await prisma.clientContract.update({
|
||||
where: { id },
|
||||
data: {
|
||||
contractNumber: input.contractNumber,
|
||||
contractDate: input.contractDate,
|
||||
name: input.name,
|
||||
ourLegalEntity: input.ourLegalEntity,
|
||||
clientLegalEntity: input.clientLegalEntity,
|
||||
balance: input.balance,
|
||||
currency: input.currency,
|
||||
isActive: input.isActive,
|
||||
isDefault: input.isDefault,
|
||||
contractType: input.contractType,
|
||||
relationship: input.relationship,
|
||||
paymentDelay: input.paymentDelay,
|
||||
creditLimit: input.creditLimit,
|
||||
delayDays: input.delayDays,
|
||||
fileUrl: input.fileUrl
|
||||
}
|
||||
})
|
||||
|
||||
return contract
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления договора:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить договор')
|
||||
}
|
||||
},
|
||||
|
||||
deleteClientContract: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
if (!context.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
await prisma.clientContract.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления договора:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить договор')
|
||||
}
|
||||
},
|
||||
|
||||
updateContractBalance: async (_: unknown, { contractId, amount, comment }: { contractId: string; amount: number; comment?: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.userId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Находим договор
|
||||
const contract = await prisma.clientContract.findUnique({
|
||||
where: { id: contractId }
|
||||
})
|
||||
|
||||
if (!contract) {
|
||||
throw new Error('Договор не найден')
|
||||
}
|
||||
|
||||
// Обновляем баланс договора
|
||||
const newBalance = contract.balance + amount
|
||||
const updatedContract = await prisma.clientContract.update({
|
||||
where: { id: contractId },
|
||||
data: { balance: newBalance }
|
||||
})
|
||||
|
||||
// Создаем запись в истории изменений баланса клиента
|
||||
await prisma.clientBalanceHistory.create({
|
||||
data: {
|
||||
clientId: contract.clientId,
|
||||
userId: actualContext.userId,
|
||||
oldValue: contract.balance,
|
||||
newValue: newBalance,
|
||||
comment: comment || `Пополнение баланса договора ${contract.contractNumber} на ${amount} ${contract.currency}`
|
||||
}
|
||||
})
|
||||
|
||||
return updatedContract
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления баланса договора:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить баланс договора')
|
||||
}
|
||||
},
|
||||
|
||||
// Счета на пополнение баланса
|
||||
createBalanceInvoice: async (_: unknown, { contractId, amount }: { contractId: string; amount: number }, context: Context) => {
|
||||
try {
|
||||
console.log('🔍 createBalanceInvoice: начало выполнения')
|
||||
console.log('📋 createBalanceInvoice: contractId:', contractId, 'amount:', amount)
|
||||
|
||||
const actualContext = context || getContext()
|
||||
console.log('🔑 createBalanceInvoice: контекст:', {
|
||||
clientId: actualContext.clientId,
|
||||
userId: actualContext.userId,
|
||||
userRole: actualContext.userRole
|
||||
})
|
||||
|
||||
if (!actualContext.clientId) {
|
||||
console.log('❌ createBalanceInvoice: клиент не авторизован')
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Находим договор и проверяем что он принадлежит клиенту
|
||||
console.log('🔍 createBalanceInvoice: поиск договора:', contractId)
|
||||
const contract = await prisma.clientContract.findUnique({
|
||||
where: { id: contractId },
|
||||
include: {
|
||||
client: {
|
||||
include: {
|
||||
legalEntities: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!contract) {
|
||||
console.log('❌ createBalanceInvoice: договор не найден')
|
||||
throw new Error('Договор не найден')
|
||||
}
|
||||
|
||||
console.log('📋 createBalanceInvoice: найден договор:', {
|
||||
id: contract.id,
|
||||
contractNumber: contract.contractNumber,
|
||||
clientId: contract.clientId,
|
||||
isActive: contract.isActive
|
||||
})
|
||||
|
||||
if (contract.clientId !== actualContext.clientId) {
|
||||
console.log('❌ createBalanceInvoice: недостаточно прав. Договор принадлежит:', contract.clientId, 'а запрашивает:', actualContext.clientId)
|
||||
throw new Error('Недостаточно прав')
|
||||
}
|
||||
|
||||
if (!contract.isActive) {
|
||||
console.log('❌ createBalanceInvoice: договор неактивен')
|
||||
throw new Error('Договор неактивен')
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
console.log('❌ createBalanceInvoice: неправильная сумма:', amount)
|
||||
throw new Error('Сумма должна быть больше 0')
|
||||
}
|
||||
|
||||
console.log('✅ createBalanceInvoice: все проверки пройдены, создаем счет')
|
||||
|
||||
// Импортируем сервис генерации счетов
|
||||
const { InvoiceService } = await import('../invoice-service')
|
||||
|
||||
// Находим юридическое лицо клиента для этого договора
|
||||
const clientLegalEntity = contract.client.legalEntities.find(le =>
|
||||
le.shortName === contract.clientLegalEntity ||
|
||||
le.fullName === contract.clientLegalEntity
|
||||
)
|
||||
|
||||
console.log('🏢 createBalanceInvoice: юридическое лицо:', clientLegalEntity?.shortName || 'не найдено')
|
||||
|
||||
// Создаем данные для счета
|
||||
const invoiceData = {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
currency: contract.currency,
|
||||
invoiceNumber: '', // будет сгенерирован в сервисе
|
||||
contractNumber: contract.contractNumber,
|
||||
clientName: clientLegalEntity?.shortName || contract.client.name,
|
||||
clientInn: clientLegalEntity?.inn
|
||||
}
|
||||
|
||||
// Генерируем номер счета
|
||||
const invoiceNumber = InvoiceService.generateInvoiceNumber()
|
||||
const expiresAt = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000) // +3 дня
|
||||
|
||||
console.log('📄 createBalanceInvoice: создаем счет с номером:', invoiceNumber)
|
||||
|
||||
// Сохраняем счет в базу данных
|
||||
const balanceInvoice = await prisma.balanceInvoice.create({
|
||||
data: {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
currency: contract.currency,
|
||||
invoiceNumber,
|
||||
qrCode: '', // Заполним позже
|
||||
expiresAt,
|
||||
status: 'PENDING'
|
||||
},
|
||||
include: {
|
||||
contract: {
|
||||
include: {
|
||||
client: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log('✅ createBalanceInvoice: счет создан успешно:', balanceInvoice.id)
|
||||
return balanceInvoice
|
||||
} catch (error) {
|
||||
console.error('❌ createBalanceInvoice: ошибка создания счета на пополнение баланса:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать счет на пополнение баланса')
|
||||
}
|
||||
},
|
||||
|
||||
updateInvoiceStatus: async (_: any, { invoiceId, status }: { invoiceId: string; status: string }, context: any) => {
|
||||
console.log('updateInvoiceStatus резолвер вызван:', { invoiceId, status });
|
||||
|
||||
if (!context.userId || context.userRole !== 'ADMIN') {
|
||||
throw new Error('Доступ запрещен. Требуются права администратора.');
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedInvoice = await prisma.balanceInvoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: {
|
||||
status: status as any,
|
||||
updatedAt: new Date()
|
||||
},
|
||||
include: {
|
||||
contract: {
|
||||
include: {
|
||||
client: {
|
||||
include: {
|
||||
legalEntities: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Если статус изменился на PAID, пополняем баланс
|
||||
if (status === 'PAID') {
|
||||
await prisma.clientContract.update({
|
||||
where: { id: updatedInvoice.contractId },
|
||||
data: {
|
||||
balance: {
|
||||
increment: updatedInvoice.amount
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Баланс пополнен на ${updatedInvoice.amount} руб. для договора ${updatedInvoice.contractId}`);
|
||||
}
|
||||
|
||||
return updatedInvoice;
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления статуса счета:', error);
|
||||
throw new Error('Не удалось обновить статус счета');
|
||||
}
|
||||
},
|
||||
|
||||
getInvoicePDF: async (_: any, { invoiceId }: { invoiceId: string }, context: any) => {
|
||||
console.log('🔍 Получение PDF счета через GraphQL:', invoiceId);
|
||||
|
||||
try {
|
||||
// Получаем счет из базы данных
|
||||
const invoice = await prisma.balanceInvoice.findUnique({
|
||||
where: { id: invoiceId },
|
||||
include: {
|
||||
contract: {
|
||||
include: {
|
||||
client: {
|
||||
include: {
|
||||
legalEntities: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!invoice) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Счет не найден'
|
||||
};
|
||||
}
|
||||
|
||||
// Проверяем авторизацию
|
||||
let hasAccess = false;
|
||||
|
||||
console.log('🔍 Проверка доступа:', {
|
||||
userId: context.userId,
|
||||
userRole: context.userRole,
|
||||
clientId: context.clientId,
|
||||
invoiceClientId: invoice.contract.clientId
|
||||
});
|
||||
|
||||
// Админ имеет доступ ко всем счетам
|
||||
if (context.userId && context.userRole === 'ADMIN') {
|
||||
hasAccess = true;
|
||||
console.log('✅ Доступ предоставлен администратору');
|
||||
}
|
||||
// Клиент имеет доступ только к своим счетам
|
||||
else if (context.clientId && context.clientId === invoice.contract.clientId) {
|
||||
hasAccess = true;
|
||||
console.log('✅ Доступ предоставлен владельцу счета');
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Доступ запрещен'
|
||||
};
|
||||
}
|
||||
|
||||
// Преобразуем данные для генерации PDF
|
||||
const legalEntity = invoice.contract.client.legalEntities[0];
|
||||
const invoiceData = {
|
||||
invoiceNumber: invoice.invoiceNumber,
|
||||
amount: invoice.amount,
|
||||
clientName: legalEntity?.shortName || invoice.contract.client.name,
|
||||
clientInn: legalEntity?.inn,
|
||||
clientAddress: legalEntity?.legalAddress,
|
||||
contractNumber: invoice.contract.contractNumber,
|
||||
description: `Пополнение баланса по договору ${invoice.contract.contractNumber}`,
|
||||
dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 дней
|
||||
};
|
||||
|
||||
// Генерируем PDF
|
||||
const pdfBuffer = await InvoiceService.generatePDF(invoiceData);
|
||||
const pdfBase64 = pdfBuffer.toString('base64');
|
||||
const filename = `Счет-${invoice.invoiceNumber}.pdf`;
|
||||
|
||||
console.log('✅ PDF успешно сгенерирован');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
pdfBase64,
|
||||
filename
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка генерации PDF:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Ошибка генерации PDF: ' + (error as Error).message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Юридические лица
|
||||
createClientLegalEntity: async (_: unknown, { clientId, input }: { clientId: string; input: ClientLegalEntityInput }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
// Проверяем авторизацию - либо админ CMS, либо клиент
|
||||
if (!actualContext.userId && !actualContext.clientId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Если это клиент, он может создавать только свои юр. лица
|
||||
if (actualContext.clientId && clientId !== actualContext.clientId) {
|
||||
throw new Error('Недостаточно прав')
|
||||
}
|
||||
|
||||
const legalEntity = await prisma.clientLegalEntity.create({
|
||||
data: {
|
||||
clientId,
|
||||
shortName: input.shortName,
|
||||
fullName: input.fullName || input.shortName,
|
||||
form: input.form || 'ООО',
|
||||
legalAddress: input.legalAddress || '',
|
||||
actualAddress: input.actualAddress,
|
||||
taxSystem: input.taxSystem || 'УСН',
|
||||
responsiblePhone: input.responsiblePhone,
|
||||
responsiblePosition: input.responsiblePosition,
|
||||
responsibleName: input.responsibleName,
|
||||
accountant: input.accountant,
|
||||
signatory: input.signatory,
|
||||
registrationReasonCode: input.registrationReasonCode,
|
||||
ogrn: input.ogrn,
|
||||
inn: input.inn,
|
||||
vatPercent: input.vatPercent || 20
|
||||
},
|
||||
include: {
|
||||
bankDetails: true
|
||||
}
|
||||
})
|
||||
|
||||
return legalEntity
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания юридического лица:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать юридическое лицо')
|
||||
}
|
||||
},
|
||||
|
||||
updateClientLegalEntity: async (_: unknown, { id, input }: { id: string; input: ClientLegalEntityInput }, context: Context) => {
|
||||
try {
|
||||
// Если контекст не передан как параметр, получаем из глобальной переменной
|
||||
const actualContext = context || getContext()
|
||||
// Проверяем авторизацию - либо админ CMS, либо клиент
|
||||
if (!actualContext.userId && !actualContext.clientId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Если это клиент, проверяем что юр. лицо принадлежит ему
|
||||
if (actualContext.clientId) {
|
||||
const existingEntity = await prisma.clientLegalEntity.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
if (!existingEntity || existingEntity.clientId !== actualContext.clientId) {
|
||||
throw new Error('Недостаточно прав')
|
||||
}
|
||||
}
|
||||
|
||||
const legalEntity = await prisma.clientLegalEntity.update({
|
||||
where: { id },
|
||||
data: {
|
||||
shortName: input.shortName,
|
||||
fullName: input.fullName,
|
||||
form: input.form,
|
||||
legalAddress: input.legalAddress,
|
||||
actualAddress: input.actualAddress,
|
||||
taxSystem: input.taxSystem,
|
||||
responsiblePhone: input.responsiblePhone,
|
||||
responsiblePosition: input.responsiblePosition,
|
||||
responsibleName: input.responsibleName,
|
||||
accountant: input.accountant,
|
||||
signatory: input.signatory,
|
||||
registrationReasonCode: input.registrationReasonCode,
|
||||
ogrn: input.ogrn,
|
||||
inn: input.inn,
|
||||
vatPercent: input.vatPercent
|
||||
},
|
||||
include: {
|
||||
bankDetails: true
|
||||
}
|
||||
})
|
||||
|
||||
return legalEntity
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления юридического лица:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить юридическое лицо')
|
||||
}
|
||||
},
|
||||
|
||||
deleteClientLegalEntity: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
// Проверяем авторизацию - либо админ CMS, либо клиент
|
||||
if (!actualContext.userId && !actualContext.clientId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Если это клиент, проверяем что юр. лицо принадлежит ему
|
||||
if (actualContext.clientId) {
|
||||
const existingEntity = await prisma.clientLegalEntity.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
if (!existingEntity || existingEntity.clientId !== actualContext.clientId) {
|
||||
throw new Error('Недостаточно прав')
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.clientLegalEntity.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления юридического лица:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить юридическое лицо')
|
||||
}
|
||||
},
|
||||
|
||||
// Банковские реквизиты
|
||||
createClientBankDetails: async (_: unknown, { legalEntityId, input }: { legalEntityId: string; input: ClientBankDetailsInput }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.userId && !actualContext.clientId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Получаем clientId из legalEntity
|
||||
const legalEntity = await prisma.clientLegalEntity.findUnique({
|
||||
where: { id: legalEntityId }
|
||||
})
|
||||
|
||||
if (!legalEntity) {
|
||||
throw new Error('Юридическое лицо не найдено')
|
||||
}
|
||||
|
||||
const bankDetails = await prisma.clientBankDetails.create({
|
||||
data: {
|
||||
clientId: legalEntity.clientId,
|
||||
legalEntityId: legalEntityId,
|
||||
name: input.name,
|
||||
accountNumber: input.accountNumber,
|
||||
bankName: input.bankName,
|
||||
bik: input.bik,
|
||||
correspondentAccount: input.correspondentAccount
|
||||
},
|
||||
include: {
|
||||
legalEntity: true
|
||||
}
|
||||
})
|
||||
|
||||
return bankDetails
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания банковских реквизитов:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать банковские реквизиты')
|
||||
}
|
||||
},
|
||||
|
||||
updateClientBankDetails: async (_: unknown, { id, input, legalEntityId }: { id: string; input: ClientBankDetailsInput; legalEntityId?: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.userId && !actualContext.clientId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
// Если передан legalEntityId, проверяем что юридическое лицо существует
|
||||
if (legalEntityId) {
|
||||
const legalEntity = await prisma.clientLegalEntity.findUnique({
|
||||
where: { id: legalEntityId }
|
||||
})
|
||||
|
||||
if (!legalEntity) {
|
||||
throw new Error('Юридическое лицо не найдено')
|
||||
}
|
||||
}
|
||||
|
||||
const bankDetails = await prisma.clientBankDetails.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name,
|
||||
accountNumber: input.accountNumber,
|
||||
bankName: input.bankName,
|
||||
bik: input.bik,
|
||||
correspondentAccount: input.correspondentAccount,
|
||||
...(legalEntityId && { legalEntityId })
|
||||
},
|
||||
include: {
|
||||
legalEntity: true
|
||||
}
|
||||
})
|
||||
|
||||
return bankDetails
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления банковских реквизитов:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить банковские реквизиты')
|
||||
}
|
||||
},
|
||||
|
||||
deleteClientBankDetails: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.userId && !actualContext.clientId) {
|
||||
throw new Error('Пользователь не авторизован')
|
||||
}
|
||||
|
||||
await prisma.clientBankDetails.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления банковских реквизитов:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить банковские реквизиты')
|
||||
}
|
||||
},
|
||||
|
||||
// Авторизация клиентов
|
||||
checkClientByPhone: async (_: unknown, { phone }: { phone: string }) => {
|
||||
try {
|
||||
const client = await prisma.client.findFirst({
|
||||
where: { phone },
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
})
|
||||
|
||||
const sessionId = Math.random().toString(36).substring(7)
|
||||
|
||||
return {
|
||||
exists: !!client,
|
||||
client,
|
||||
sessionId
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки клиента по телефону:', error)
|
||||
throw new Error('Не удалось проверить клиента')
|
||||
}
|
||||
},
|
||||
|
||||
sendSMSCode: async (_: unknown, { phone, sessionId }: { phone: string; sessionId?: string }) => {
|
||||
try {
|
||||
// Используем импортированные сервисы
|
||||
|
||||
const finalSessionId = sessionId || Math.random().toString(36).substring(7)
|
||||
|
||||
// Проверяем, есть ли уже активный код для этого номера и сессии
|
||||
if (smsCodeStore.hasActiveCode(phone, finalSessionId)) {
|
||||
const ttl = smsCodeStore.getCodeTTL(phone, finalSessionId)
|
||||
console.log(`У номера ${phone} уже есть активный код, осталось ${ttl} секунд`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionId: finalSessionId,
|
||||
message: `Код уже отправлен. Попробуйте через ${ttl} секунд.`
|
||||
}
|
||||
}
|
||||
|
||||
// Генерируем 5-значный код
|
||||
const code = Math.floor(10000 + Math.random() * 90000).toString()
|
||||
|
||||
// Сохраняем код в хранилище
|
||||
smsCodeStore.saveCode(phone, code, finalSessionId)
|
||||
|
||||
// Отправляем SMS через Билайн API
|
||||
const smsResult = await smsService.sendVerificationCode(phone, code)
|
||||
|
||||
if (smsResult.success) {
|
||||
return {
|
||||
success: true,
|
||||
sessionId: finalSessionId,
|
||||
messageId: smsResult.messageId,
|
||||
message: 'SMS код отправлен'
|
||||
}
|
||||
} else {
|
||||
// Если SMS не отправилось в production - бросаем ошибку
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(`Не удалось отправить SMS: ${smsResult.error}`)
|
||||
}
|
||||
|
||||
// В development режиме возвращаем успех и показываем код
|
||||
return {
|
||||
success: true,
|
||||
sessionId: finalSessionId,
|
||||
message: 'SMS отправлен (dev mode)',
|
||||
code // Только в dev режиме!
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка отправки SMS:', error)
|
||||
throw new Error('Не удалось отправить SMS код')
|
||||
}
|
||||
},
|
||||
|
||||
verifyCode: async (_: unknown, { phone, code, sessionId }: { phone: string; code: string; sessionId: string }) => {
|
||||
try {
|
||||
console.log(`Верификация кода для ${phone}, код: ${code}, sessionId: ${sessionId}`)
|
||||
|
||||
// Проверяем код через наше хранилище
|
||||
const verification = smsCodeStore.verifyCode(phone, code, sessionId)
|
||||
|
||||
if (!verification.valid) {
|
||||
console.log(`Код неверный: ${verification.error}`)
|
||||
throw new Error(verification.error || 'Неверный код')
|
||||
}
|
||||
|
||||
console.log('Код верифицирован успешно')
|
||||
|
||||
// Ищем клиента в базе
|
||||
const client = await prisma.client.findFirst({
|
||||
where: { phone },
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Клиент найден: ${!!client}`)
|
||||
|
||||
if (client) {
|
||||
// Если клиент существует - авторизуем его
|
||||
console.log(`Авторизуем существующего клиента: ${client.id}`)
|
||||
const token = `client_${client.id}_${Date.now()}`
|
||||
|
||||
return {
|
||||
success: true,
|
||||
client,
|
||||
token
|
||||
}
|
||||
} else {
|
||||
// Если клиент не существует - возвращаем успех без клиента
|
||||
// Это означает что нужно будет перейти к регистрации
|
||||
console.log('Клиент не найден, возвращаем success с client: null для регистрации')
|
||||
return {
|
||||
success: true,
|
||||
client: null,
|
||||
token: null
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка верификации кода:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось верифицировать код')
|
||||
}
|
||||
},
|
||||
|
||||
registerNewClient: async (_: unknown, { phone, name }: { phone: string; name: string; sessionId: string }) => {
|
||||
try {
|
||||
// Проверяем, что клиент еще не существует
|
||||
const existingClient = await prisma.client.findFirst({
|
||||
where: { phone }
|
||||
})
|
||||
|
||||
if (existingClient) {
|
||||
throw new Error('Клиент с таким номером уже существует')
|
||||
}
|
||||
|
||||
// Разбиваем имя на имя и фамилию
|
||||
const nameParts = name.trim().split(' ')
|
||||
const firstName = nameParts[0] || name
|
||||
const lastName = nameParts.slice(1).join(' ') || ''
|
||||
const fullName = lastName ? `${firstName} ${lastName}` : firstName
|
||||
|
||||
// Создаем нового клиента
|
||||
const client = await prisma.client.create({
|
||||
data: {
|
||||
clientNumber: `CL${Date.now()}`,
|
||||
type: 'INDIVIDUAL',
|
||||
name: fullName,
|
||||
phone,
|
||||
isConfirmed: true,
|
||||
balance: 0,
|
||||
emailNotifications: false,
|
||||
smsNotifications: false,
|
||||
pushNotifications: false
|
||||
},
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
})
|
||||
|
||||
// Создаем простой токен
|
||||
const token = `client_${client.id}_${Date.now()}`
|
||||
|
||||
return {
|
||||
success: true,
|
||||
client,
|
||||
token
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка регистрации клиента:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось зарегистрировать клиента')
|
||||
}
|
||||
},
|
||||
|
||||
// Мутации для гаража клиентов
|
||||
createUserVehicle: async (_: unknown, { input }: { input: ClientVehicleInput }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Проверяем существует ли клиент, если нет - создаем только для временных клиентов
|
||||
let client = await prisma.client.findUnique({
|
||||
where: { id: actualContext.clientId }
|
||||
})
|
||||
|
||||
if (!client) {
|
||||
if (actualContext.clientId.startsWith('client_') && actualContext.clientId.length > 30) {
|
||||
client = await prisma.client.create({
|
||||
data: {
|
||||
id: actualContext.clientId,
|
||||
clientNumber: `CLIENT_${Date.now()}`,
|
||||
type: 'INDIVIDUAL',
|
||||
name: 'Гость',
|
||||
phone: '+7',
|
||||
isConfirmed: false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error('Клиент не найден в системе')
|
||||
}
|
||||
}
|
||||
|
||||
const vehicle = await prisma.clientVehicle.create({
|
||||
data: {
|
||||
clientId: actualContext.clientId,
|
||||
name: input.name,
|
||||
vin: input.vin,
|
||||
frame: input.frame,
|
||||
licensePlate: input.licensePlate,
|
||||
brand: input.brand,
|
||||
model: input.model,
|
||||
modification: input.modification,
|
||||
year: input.year,
|
||||
mileage: input.mileage,
|
||||
comment: input.comment
|
||||
}
|
||||
})
|
||||
|
||||
return vehicle
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания автомобиля:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать автомобиль')
|
||||
}
|
||||
},
|
||||
|
||||
updateUserVehicle: async (_: unknown, { id, input }: { id: string; input: ClientVehicleInput }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Проверяем, что автомобиль принадлежит клиенту
|
||||
const existingVehicle = await prisma.clientVehicle.findFirst({
|
||||
where: { id, clientId: actualContext.clientId }
|
||||
})
|
||||
|
||||
if (!existingVehicle) {
|
||||
throw new Error('Автомобиль не найден')
|
||||
}
|
||||
|
||||
const vehicle = await prisma.clientVehicle.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name,
|
||||
vin: input.vin,
|
||||
frame: input.frame,
|
||||
licensePlate: input.licensePlate,
|
||||
brand: input.brand,
|
||||
model: input.model,
|
||||
modification: input.modification,
|
||||
year: input.year,
|
||||
mileage: input.mileage,
|
||||
comment: input.comment
|
||||
}
|
||||
})
|
||||
|
||||
return vehicle
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления автомобиля:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить автомобиль')
|
||||
}
|
||||
},
|
||||
|
||||
deleteUserVehicle: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Проверяем, что автомобиль принадлежит клиенту
|
||||
const existingVehicle = await prisma.clientVehicle.findFirst({
|
||||
where: { id, clientId: actualContext.clientId }
|
||||
})
|
||||
|
||||
if (!existingVehicle) {
|
||||
throw new Error('Автомобиль не найден')
|
||||
}
|
||||
|
||||
await prisma.clientVehicle.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления автомобиля:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить автомобиль')
|
||||
}
|
||||
},
|
||||
|
||||
addVehicleFromSearch: async (_: unknown, { vin, comment }: { vin: string; comment?: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Определяем clientId, убирая префикс client_ если он есть
|
||||
const clientIdParts = actualContext.clientId.split('_')
|
||||
let clientId = actualContext.clientId
|
||||
|
||||
if (clientIdParts.length >= 3) {
|
||||
clientId = clientIdParts[1]
|
||||
} else if (clientIdParts.length === 2) {
|
||||
clientId = clientIdParts[1]
|
||||
}
|
||||
|
||||
// Ищем информацию об автомобиле в истории поиска
|
||||
const searchHistoryItem = await prisma.partsSearchHistory.findFirst({
|
||||
where: {
|
||||
clientId,
|
||||
searchQuery: vin,
|
||||
searchType: 'VIN'
|
||||
},
|
||||
orderBy: { createdAt: 'desc' } // Берем самую свежую запись
|
||||
})
|
||||
|
||||
// Создаем название автомобиля на основе данных из истории
|
||||
let vehicleName = `Автомобиль ${vin}`
|
||||
let vehicleBrand: string | undefined = undefined
|
||||
let vehicleModel: string | undefined = undefined
|
||||
let vehicleYear: number | undefined = undefined
|
||||
|
||||
if (searchHistoryItem && (searchHistoryItem.vehicleBrand || searchHistoryItem.vehicleModel)) {
|
||||
vehicleBrand = searchHistoryItem.vehicleBrand || undefined
|
||||
vehicleModel = searchHistoryItem.vehicleModel || undefined
|
||||
vehicleYear = searchHistoryItem.vehicleYear || undefined
|
||||
|
||||
// Формируем красивое название
|
||||
if (vehicleBrand && vehicleModel) {
|
||||
vehicleName = `${vehicleBrand} ${vehicleModel}`
|
||||
} else if (vehicleBrand) {
|
||||
vehicleName = vehicleBrand
|
||||
} else if (vehicleModel) {
|
||||
vehicleName = vehicleModel
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем автомобиль из результата поиска с полной информацией
|
||||
const vehicle = await prisma.clientVehicle.create({
|
||||
data: {
|
||||
clientId: actualContext.clientId,
|
||||
name: vehicleName,
|
||||
vin,
|
||||
brand: vehicleBrand,
|
||||
model: vehicleModel,
|
||||
year: vehicleYear,
|
||||
comment: comment || ''
|
||||
}
|
||||
})
|
||||
|
||||
return vehicle
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления автомобиля из поиска:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось добавить автомобиль из поиска')
|
||||
}
|
||||
},
|
||||
|
||||
deleteSearchHistoryItem: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Определяем clientId, убирая префикс client_ если он есть
|
||||
const clientIdParts = actualContext.clientId.split('_')
|
||||
let clientId = actualContext.clientId
|
||||
|
||||
if (clientIdParts.length >= 3) {
|
||||
clientId = clientIdParts[1]
|
||||
} else if (clientIdParts.length === 2) {
|
||||
clientId = clientIdParts[1]
|
||||
}
|
||||
|
||||
console.log('deleteSearchHistoryItem: удаление VIN записи', id, 'для клиента', clientId)
|
||||
|
||||
// Проверяем, что запись принадлежит клиенту и имеет тип VIN
|
||||
const existingItem = await prisma.partsSearchHistory.findFirst({
|
||||
where: {
|
||||
id,
|
||||
clientId,
|
||||
searchType: 'VIN' // Удаляем только VIN записи
|
||||
}
|
||||
})
|
||||
|
||||
if (!existingItem) {
|
||||
throw new Error('VIN запись не найдена или не принадлежит клиенту')
|
||||
}
|
||||
|
||||
await prisma.partsSearchHistory.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
console.log('deleteSearchHistoryItem: VIN запись удалена')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления из истории VIN поиска:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить элемент из истории поиска')
|
||||
}
|
||||
},
|
||||
|
||||
// Мутации для истории поиска запчастей
|
||||
deletePartsSearchHistoryItem: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Определяем clientId, убирая префикс client_ если он есть
|
||||
const clientIdParts = actualContext.clientId.split('_')
|
||||
let clientId = actualContext.clientId
|
||||
|
||||
if (clientIdParts.length >= 3) {
|
||||
clientId = clientIdParts[1]
|
||||
} else if (clientIdParts.length === 2) {
|
||||
clientId = clientIdParts[1]
|
||||
}
|
||||
|
||||
console.log('deletePartsSearchHistoryItem: удаление записи', id, 'для клиента', clientId)
|
||||
|
||||
// Проверяем, что запись принадлежит клиенту
|
||||
const existingItem = await prisma.partsSearchHistory.findFirst({
|
||||
where: { id, clientId }
|
||||
})
|
||||
|
||||
if (!existingItem) {
|
||||
throw new Error('Запись не найдена или не принадлежит клиенту')
|
||||
}
|
||||
|
||||
await prisma.partsSearchHistory.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
console.log('deletePartsSearchHistoryItem: запись удалена')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления записи истории поиска запчастей:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить запись из истории поиска')
|
||||
}
|
||||
},
|
||||
|
||||
clearPartsSearchHistory: async (_: unknown, __: unknown, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Определяем clientId, убирая префикс client_ если он есть
|
||||
const clientIdParts = actualContext.clientId.split('_')
|
||||
let clientId = actualContext.clientId
|
||||
|
||||
if (clientIdParts.length >= 3) {
|
||||
clientId = clientIdParts[1]
|
||||
} else if (clientIdParts.length === 2) {
|
||||
clientId = clientIdParts[1]
|
||||
}
|
||||
|
||||
console.log('clearPartsSearchHistory: очистка истории для клиента', clientId)
|
||||
|
||||
const deleteResult = await prisma.partsSearchHistory.deleteMany({
|
||||
where: { clientId }
|
||||
})
|
||||
|
||||
console.log(`clearPartsSearchHistory: удалено ${deleteResult.count} записей`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка очистки истории поиска запчастей:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось очистить историю поиска')
|
||||
}
|
||||
},
|
||||
|
||||
createPartsSearchHistoryItem: async (_: unknown, { input }: { input: any }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Определяем clientId, убирая префикс client_ если он есть
|
||||
const clientIdParts = actualContext.clientId.split('_')
|
||||
let clientId = actualContext.clientId
|
||||
|
||||
if (clientIdParts.length >= 3) {
|
||||
clientId = clientIdParts[1]
|
||||
} else if (clientIdParts.length === 2) {
|
||||
clientId = clientIdParts[1]
|
||||
}
|
||||
|
||||
console.log('createPartsSearchHistoryItem: создание записи для клиента', clientId)
|
||||
|
||||
// Проверяем существует ли клиент
|
||||
const client = await prisma.client.findUnique({
|
||||
where: { id: clientId }
|
||||
})
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Клиент не найден')
|
||||
}
|
||||
|
||||
const historyItem = await prisma.partsSearchHistory.create({
|
||||
data: {
|
||||
clientId,
|
||||
searchQuery: input.searchQuery,
|
||||
searchType: input.searchType,
|
||||
brand: input.brand,
|
||||
articleNumber: input.articleNumber,
|
||||
vehicleBrand: input.vehicleBrand,
|
||||
vehicleModel: input.vehicleModel,
|
||||
vehicleYear: input.vehicleYear,
|
||||
resultCount: input.resultCount || 0
|
||||
}
|
||||
})
|
||||
|
||||
console.log('createPartsSearchHistoryItem: запись создана', historyItem.id)
|
||||
|
||||
return {
|
||||
id: historyItem.id,
|
||||
searchQuery: historyItem.searchQuery,
|
||||
searchType: historyItem.searchType,
|
||||
brand: historyItem.brand,
|
||||
articleNumber: historyItem.articleNumber,
|
||||
vehicleInfo: historyItem.vehicleBrand || historyItem.vehicleModel || historyItem.vehicleYear ? {
|
||||
brand: historyItem.vehicleBrand,
|
||||
model: historyItem.vehicleModel,
|
||||
year: historyItem.vehicleYear
|
||||
} : null,
|
||||
resultCount: historyItem.resultCount,
|
||||
createdAt: historyItem.createdAt instanceof Date ? historyItem.createdAt.toISOString() : historyItem.createdAt
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания записи истории поиска запчастей:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать запись истории поиска')
|
||||
}
|
||||
},
|
||||
|
||||
createVehicleFromVin: async (_: unknown, { vin, comment }: { vin: string; comment?: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
console.log('Создание автомобиля из VIN:', vin)
|
||||
|
||||
// Проверяем существует ли клиент, если нет - создаем только если это действительно новый клиент
|
||||
let client = await prisma.client.findUnique({
|
||||
where: { id: actualContext.clientId }
|
||||
})
|
||||
|
||||
if (!client) {
|
||||
// Проверяем, не является ли это токеном временного клиента
|
||||
// Временные клиенты имеют длинные ID типа "client_cmbzedr1k0000rqz5phpvgpxc"
|
||||
if (actualContext.clientId.startsWith('client_') && actualContext.clientId.length > 30) {
|
||||
console.log('Создаем временного клиента:', actualContext.clientId)
|
||||
client = await prisma.client.create({
|
||||
data: {
|
||||
id: actualContext.clientId,
|
||||
clientNumber: `CLIENT_${Date.now()}`,
|
||||
type: 'INDIVIDUAL',
|
||||
name: 'Гость',
|
||||
phone: '+7',
|
||||
isConfirmed: false
|
||||
}
|
||||
})
|
||||
console.log('Временный клиент создан:', client.id)
|
||||
} else {
|
||||
throw new Error('Клиент не найден в системе')
|
||||
}
|
||||
}
|
||||
|
||||
// Ищем автомобиль в Laximo
|
||||
let laximoData: any[] = []
|
||||
try {
|
||||
laximoData = await laximoService.findVehicleGlobal(vin)
|
||||
console.log('Данные из Laximo:', laximoData)
|
||||
} catch (laximoError) {
|
||||
console.log('Ошибка поиска в Laximo:', laximoError)
|
||||
// Продолжаем выполнение, создадим автомобиль без данных Laximo
|
||||
}
|
||||
|
||||
// Выбираем первый результат из Laximo или создаем базовые данные
|
||||
let vehicleData = {
|
||||
clientId: actualContext.clientId,
|
||||
vin: vin.toUpperCase(),
|
||||
comment: comment || '',
|
||||
name: `Автомобиль ${vin}`,
|
||||
brand: null as string | null,
|
||||
model: null as string | null,
|
||||
modification: null as string | null,
|
||||
year: null as number | null
|
||||
}
|
||||
|
||||
if (laximoData && laximoData.length > 0) {
|
||||
const firstResult = laximoData[0]
|
||||
vehicleData = {
|
||||
...vehicleData,
|
||||
name: firstResult.name || `${firstResult.brand || ''} ${firstResult.model || ''}`.trim() || vehicleData.name,
|
||||
brand: firstResult.brand || null,
|
||||
model: firstResult.model || null,
|
||||
modification: firstResult.modification || null,
|
||||
year: firstResult.year ? parseInt(firstResult.year, 10) : null
|
||||
}
|
||||
}
|
||||
|
||||
const vehicle = await prisma.clientVehicle.create({
|
||||
data: vehicleData
|
||||
})
|
||||
|
||||
console.log('Автомобиль создан:', vehicle)
|
||||
return vehicle
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания автомобиля из VIN:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать автомобиль из VIN')
|
||||
}
|
||||
},
|
||||
|
||||
// Обновление данных авторизованного клиента
|
||||
updateClientMe: async (_: unknown, { input }: { input: ClientInput }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
const updatedClient = await prisma.client.update({
|
||||
where: { id: actualContext.clientId },
|
||||
data: input,
|
||||
include: {
|
||||
legalEntities: true,
|
||||
profile: true,
|
||||
vehicles: true,
|
||||
deliveryAddresses: true,
|
||||
contacts: true,
|
||||
contracts: true,
|
||||
bankDetails: true,
|
||||
discounts: true
|
||||
}
|
||||
})
|
||||
|
||||
return updatedClient
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления данных клиента:', error)
|
||||
throw new Error('Не удалось обновить данные клиента')
|
||||
}
|
||||
},
|
||||
|
||||
// Создание юр. лица для авторизованного клиента
|
||||
createClientLegalEntityMe: async (_: unknown, { input }: { input: ClientLegalEntityInput }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
const legalEntity = await prisma.clientLegalEntity.create({
|
||||
data: {
|
||||
clientId: actualContext.clientId,
|
||||
shortName: input.shortName,
|
||||
fullName: input.fullName || input.shortName,
|
||||
form: input.form || 'ООО',
|
||||
legalAddress: input.legalAddress || '',
|
||||
actualAddress: input.actualAddress,
|
||||
taxSystem: input.taxSystem || 'УСН',
|
||||
responsiblePhone: input.responsiblePhone,
|
||||
responsiblePosition: input.responsiblePosition,
|
||||
responsibleName: input.responsibleName,
|
||||
accountant: input.accountant,
|
||||
signatory: input.signatory,
|
||||
registrationReasonCode: input.registrationReasonCode,
|
||||
ogrn: input.ogrn,
|
||||
inn: input.inn,
|
||||
vatPercent: input.vatPercent || 20
|
||||
},
|
||||
include: {
|
||||
bankDetails: true
|
||||
}
|
||||
})
|
||||
|
||||
return legalEntity
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания юридического лица:', error)
|
||||
throw new Error('Не удалось создать юридическое лицо')
|
||||
}
|
||||
},
|
||||
|
||||
// Адреса доставки для авторизованного клиента
|
||||
createClientDeliveryAddressMe: async (_: unknown, { input }: { input: ClientDeliveryAddressInput }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
const address = await prisma.clientDeliveryAddress.create({
|
||||
data: {
|
||||
clientId: actualContext.clientId,
|
||||
name: input.name,
|
||||
address: input.address,
|
||||
deliveryType: input.deliveryType,
|
||||
comment: input.comment,
|
||||
// Дополнительные поля для курьерской доставки
|
||||
entrance: input.entrance,
|
||||
floor: input.floor,
|
||||
apartment: input.apartment,
|
||||
intercom: input.intercom,
|
||||
deliveryTime: input.deliveryTime,
|
||||
contactPhone: input.contactPhone
|
||||
}
|
||||
})
|
||||
|
||||
return address
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания адреса доставки:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось создать адрес доставки')
|
||||
}
|
||||
},
|
||||
|
||||
updateClientDeliveryAddressMe: async (_: unknown, { id, input }: { id: string; input: ClientDeliveryAddressInput }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Проверяем, что адрес принадлежит текущему клиенту
|
||||
const existingAddress = await prisma.clientDeliveryAddress.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!existingAddress || existingAddress.clientId !== actualContext.clientId) {
|
||||
throw new Error('Адрес не найден или недостаточно прав')
|
||||
}
|
||||
|
||||
const address = await prisma.clientDeliveryAddress.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name,
|
||||
address: input.address,
|
||||
deliveryType: input.deliveryType,
|
||||
comment: input.comment,
|
||||
// Дополнительные поля для курьерской доставки
|
||||
entrance: input.entrance,
|
||||
floor: input.floor,
|
||||
apartment: input.apartment,
|
||||
intercom: input.intercom,
|
||||
deliveryTime: input.deliveryTime,
|
||||
contactPhone: input.contactPhone
|
||||
}
|
||||
})
|
||||
|
||||
return address
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления адреса доставки:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось обновить адрес доставки')
|
||||
}
|
||||
},
|
||||
|
||||
deleteClientDeliveryAddressMe: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Проверяем, что адрес принадлежит текущему клиенту
|
||||
const existingAddress = await prisma.clientDeliveryAddress.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!existingAddress || existingAddress.clientId !== actualContext.clientId) {
|
||||
throw new Error('Адрес не найден или недостаточно прав')
|
||||
}
|
||||
|
||||
await prisma.clientDeliveryAddress.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления адреса доставки:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить адрес доставки')
|
||||
}
|
||||
},
|
||||
|
||||
// Заказы и платежи
|
||||
createOrder: async (_: unknown, { input }: { input: CreateOrderInput }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
|
||||
// Проверяем наличие товаров из нашего склада и резервируем их
|
||||
const internalItems = input.items.filter(item => item.productId) // Товары с productId - это наши товары
|
||||
|
||||
if (internalItems.length > 0) {
|
||||
console.log('createOrder: проверяем наличие внутренних товаров:', internalItems.length)
|
||||
|
||||
// Проверяем наличие каждого товара
|
||||
for (const item of internalItems) {
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id: item.productId! }
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
throw new Error(`Товар с ID ${item.productId} не найден`)
|
||||
}
|
||||
|
||||
if (product.stock < item.quantity) {
|
||||
throw new Error(`Недостаточно товара "${product.name}" в наличии. Доступно: ${product.stock}, запрошено: ${item.quantity}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('createOrder: все товары доступны, резервируем')
|
||||
|
||||
// Резервируем товары (вычитаем из наличия)
|
||||
for (const item of internalItems) {
|
||||
await prisma.product.update({
|
||||
where: { id: item.productId! },
|
||||
data: {
|
||||
stock: {
|
||||
decrement: item.quantity
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log(`createOrder: зарезервировано ${item.quantity} шт. товара ${item.productId}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Генерируем номер заказа
|
||||
const orderNumber = `ORD-${Date.now()}-${Math.random().toString(36).substr(2, 9).toUpperCase()}`
|
||||
|
||||
// Вычисляем общую сумму
|
||||
const totalAmount = input.items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
||||
|
||||
// Определяем clientId, убирая префикс client_ если он есть
|
||||
const clientId = actualContext.clientId || input.clientId
|
||||
const cleanClientId = clientId && clientId.startsWith('client_')
|
||||
? clientId.substring(7)
|
||||
: clientId
|
||||
|
||||
const order = await prisma.order.create({
|
||||
data: {
|
||||
orderNumber,
|
||||
clientId: cleanClientId,
|
||||
clientEmail: input.clientEmail,
|
||||
clientPhone: input.clientPhone,
|
||||
clientName: input.clientName,
|
||||
totalAmount,
|
||||
finalAmount: totalAmount, // Пока без скидок
|
||||
deliveryAddress: input.deliveryAddress,
|
||||
comment: input.comment,
|
||||
items: {
|
||||
create: input.items.map(item => ({
|
||||
productId: item.productId,
|
||||
externalId: item.externalId,
|
||||
name: item.name,
|
||||
article: item.article,
|
||||
brand: item.brand,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
totalPrice: item.price * item.quantity
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: {
|
||||
client: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true
|
||||
}
|
||||
},
|
||||
payments: true
|
||||
}
|
||||
})
|
||||
|
||||
console.log('createOrder: заказ создан:', order.orderNumber)
|
||||
return order
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания заказа:', error)
|
||||
throw new Error('Не удалось создать заказ')
|
||||
}
|
||||
},
|
||||
|
||||
// Мутации для избранного
|
||||
addToFavorites: async (_: unknown, { input }: { input: FavoriteInput }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Удаляем префикс client_ если он есть
|
||||
const cleanClientId = actualContext.clientId.startsWith('client_')
|
||||
? actualContext.clientId.substring(7)
|
||||
: actualContext.clientId
|
||||
|
||||
// Проверяем, нет ли уже такого товара в избранном
|
||||
const existingFavorite = await prisma.favorite.findFirst({
|
||||
where: {
|
||||
clientId: cleanClientId,
|
||||
productId: input.productId || undefined,
|
||||
offerKey: input.offerKey || undefined,
|
||||
article: input.article,
|
||||
brand: input.brand
|
||||
}
|
||||
})
|
||||
|
||||
if (existingFavorite) {
|
||||
return existingFavorite
|
||||
}
|
||||
|
||||
const favorite = await prisma.favorite.create({
|
||||
data: {
|
||||
clientId: cleanClientId,
|
||||
productId: input.productId,
|
||||
offerKey: input.offerKey,
|
||||
name: input.name,
|
||||
brand: input.brand,
|
||||
article: input.article,
|
||||
price: input.price,
|
||||
currency: input.currency,
|
||||
image: input.image
|
||||
},
|
||||
include: {
|
||||
client: true
|
||||
}
|
||||
})
|
||||
|
||||
return favorite
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления в избранное:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось добавить товар в избранное')
|
||||
}
|
||||
},
|
||||
|
||||
removeFromFavorites: async (_: unknown, { id }: { id: string }, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Удаляем префикс client_ если он есть
|
||||
const cleanClientId = actualContext.clientId.startsWith('client_')
|
||||
? actualContext.clientId.substring(7)
|
||||
: actualContext.clientId
|
||||
|
||||
// Проверяем, что товар принадлежит текущему клиенту
|
||||
const existingFavorite = await prisma.favorite.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!existingFavorite || existingFavorite.clientId !== cleanClientId) {
|
||||
throw new Error('Товар не найден в избранном или недостаточно прав')
|
||||
}
|
||||
|
||||
await prisma.favorite.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления из избранного:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось удалить товар из избранного')
|
||||
}
|
||||
},
|
||||
|
||||
clearFavorites: async (_: unknown, _args: unknown, context: Context) => {
|
||||
try {
|
||||
const actualContext = context || getContext()
|
||||
if (!actualContext.clientId) {
|
||||
throw new Error('Клиент не авторизован')
|
||||
}
|
||||
|
||||
// Удаляем префикс client_ если он есть
|
||||
const cleanClientId = actualContext.clientId.startsWith('client_')
|
||||
? actualContext.clientId.substring(7)
|
||||
: actualContext.clientId
|
||||
|
||||
await prisma.favorite.deleteMany({
|
||||
where: {
|
||||
clientId: cleanClientId
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Ошибка очистки избранного:', error)
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('Не удалось очистить избранное')
|
||||
}
|
||||
},
|
||||
|
||||
// Мутация для получения офферов доставки
|
||||
getDeliveryOffers: async (_: unknown, { input }: {
|
||||
input: {
|
||||
items: Array<{
|
||||
name: string;
|
||||
article?: string;
|
||||
brand?: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
weight?: number;
|
||||
dimensions?: string;
|
||||
deliveryTime?: number; // Срок доставки товара к нам на склад
|
||||
offerKey?: string; // Для внешних товаров
|
||||
isExternal?: boolean; // Флаг внешнего товара
|
||||
}>;
|
||||
deliveryAddress: string;
|
||||
recipientName: string;
|
||||
recipientPhone: string;
|
||||
}
|
||||
}, context: Context) => {
|
||||
// Вычисляем максимальный срок доставки товаров к нам на склад (вне try блока для доступа в catch)
|
||||
const maxSupplierDeliveryDays = Math.max(
|
||||
...input.items.map(item => item.deliveryTime || 0)
|
||||
);
|
||||
|
||||
try {
|
||||
console.log('🚚 Получение офферов доставки для:', input.deliveryAddress)
|
||||
|
||||
console.log('📦 Максимальный срок поставки товаров на склад:', maxSupplierDeliveryDays, 'дней')
|
||||
console.log('📋 Товары в заказе:', input.items.map(item => ({
|
||||
name: item.name,
|
||||
article: item.article,
|
||||
deliveryTime: item.deliveryTime,
|
||||
isExternal: item.isExternal
|
||||
})))
|
||||
|
||||
// Общие данные для Яндекс API
|
||||
const baseCartData = {
|
||||
items: input.items.map((item, index) => ({
|
||||
id: `item_${index}`,
|
||||
name: item.name,
|
||||
article: item.article || '',
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
weight: item.weight || 500, // 500г по умолчанию
|
||||
dimensions: item.dimensions ? { dx: 10, dy: 10, dz: 5 } : { dx: 10, dy: 10, dz: 5 }, // размеры по умолчанию
|
||||
deliveryTime: item.deliveryTime || 0, // Передаем срок поставки товара
|
||||
})),
|
||||
deliveryAddress: input.deliveryAddress,
|
||||
recipientName: input.recipientName,
|
||||
recipientPhone: input.recipientPhone,
|
||||
paymentMethod: 'already_paid' as const, // По умолчанию оплата уже произведена
|
||||
maxSupplierDeliveryDays: maxSupplierDeliveryDays, // Передаем максимальный срок поставки
|
||||
}
|
||||
|
||||
const allOffers: any[] = []
|
||||
|
||||
// 1. Пробуем курьерскую доставку
|
||||
try {
|
||||
console.log('🚚 Пробуем курьерскую доставку...')
|
||||
const courierData = { ...baseCartData, deliveryType: 'courier' as const }
|
||||
const courierOffers = await yandexDeliveryService.createOfferFromCart(courierData)
|
||||
|
||||
if (courierOffers.offers && courierOffers.offers.length > 0) {
|
||||
console.log(`✅ Найдено ${courierOffers.offers.length} офферов курьерской доставки`)
|
||||
allOffers.push(...courierOffers.offers.map(offer => ({ ...offer, delivery_type: 'courier' })))
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Курьерская доставка недоступна:', error instanceof Error ? error.message : 'Неизвестная ошибка')
|
||||
}
|
||||
|
||||
// 2. Пробуем ПВЗ
|
||||
try {
|
||||
console.log('📦 Пробуем доставку в ПВЗ...')
|
||||
const pickupData = { ...baseCartData, deliveryType: 'pickup' as const }
|
||||
const pickupOffers = await yandexDeliveryService.createOfferFromCart(pickupData)
|
||||
|
||||
if (pickupOffers.offers && pickupOffers.offers.length > 0) {
|
||||
console.log(`✅ Найдено ${pickupOffers.offers.length} офферов доставки в ПВЗ`)
|
||||
allOffers.push(...pickupOffers.offers.map(offer => ({ ...offer, delivery_type: 'pickup' })))
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Доставка в ПВЗ недоступна:', error instanceof Error ? error.message : 'Неизвестная ошибка')
|
||||
}
|
||||
|
||||
console.log('✅ Всего получено офферов:', allOffers.length)
|
||||
|
||||
// Удаляем дубликаты офферов с одинаковыми delivery_type
|
||||
const uniqueOffers = allOffers.reduce((acc, current) => {
|
||||
const existingOffer = acc.find(offer => offer.delivery_type === current.delivery_type)
|
||||
if (!existingOffer) {
|
||||
acc.push(current)
|
||||
}
|
||||
return acc
|
||||
}, [] as any[])
|
||||
|
||||
console.log(`🔄 Удалены дубликаты: ${allOffers.length} → ${uniqueOffers.length} офферов`)
|
||||
|
||||
// Форматируем офферы для фронтенда
|
||||
const formattedOffers = uniqueOffers.map((offer, index) => {
|
||||
const deliveryInterval = offer.offer_details?.delivery_interval
|
||||
const pricing = offer.offer_details?.pricing
|
||||
const deliveryType = offer.delivery_type || 'courier'
|
||||
|
||||
console.log('📅 Обработка оффера:', {
|
||||
offer_id: offer.offer_id,
|
||||
delivery_type: deliveryType,
|
||||
delivery_interval: deliveryInterval,
|
||||
pricing: pricing
|
||||
})
|
||||
|
||||
// Правильно вычисляем дату доставки с учетом срока поставки товара
|
||||
const today = new Date()
|
||||
const deliveryDate = new Date(today)
|
||||
deliveryDate.setDate(today.getDate() + maxSupplierDeliveryDays + 1) // +1 день на саму доставку
|
||||
|
||||
let deliveryTime = '10:00-18:00'
|
||||
let deliveryCost = 0
|
||||
|
||||
if (deliveryInterval && typeof deliveryInterval === 'object' && 'min' in deliveryInterval) {
|
||||
// Проверяем, если это Unix timestamp
|
||||
let minDate: Date, maxDate: Date
|
||||
|
||||
if (typeof deliveryInterval.min === 'number' && deliveryInterval.min > 1000000000) {
|
||||
// Это Unix timestamp в секундах
|
||||
minDate = new Date(deliveryInterval.min * 1000)
|
||||
maxDate = new Date(deliveryInterval.max * 1000)
|
||||
} else {
|
||||
// Это ISO строка или timestamp в миллисекундах
|
||||
minDate = new Date(deliveryInterval.min)
|
||||
maxDate = new Date(deliveryInterval.max)
|
||||
}
|
||||
|
||||
// Проверяем, что даты валидны
|
||||
if (!isNaN(minDate.getTime()) && !isNaN(maxDate.getTime())) {
|
||||
// Используем минимальную дату из интервала + время поставки товара
|
||||
const calculatedDate = new Date(minDate)
|
||||
calculatedDate.setDate(minDate.getDate() + maxSupplierDeliveryDays)
|
||||
deliveryDate.setTime(calculatedDate.getTime())
|
||||
|
||||
if (deliveryType === 'pickup') {
|
||||
deliveryTime = `С ${deliveryDate.getDate()} ${deliveryDate.toLocaleDateString('ru-RU', { month: 'long' })}`
|
||||
} else {
|
||||
deliveryTime = `${minDate.getHours().toString().padStart(2, '0')}:${minDate.getMinutes().toString().padStart(2, '0')}-${maxDate.getHours().toString().padStart(2, '0')}:${maxDate.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pricing) {
|
||||
// Парсим стоимость из строки типа "192.15 RUB"
|
||||
const match = pricing.match(/(\d+(?:\.\d+)?)/);
|
||||
if (match) {
|
||||
deliveryCost = Math.round(parseFloat(match[1]))
|
||||
}
|
||||
}
|
||||
|
||||
// Определяем название и описание в зависимости от типа доставки
|
||||
let name = 'Курьерская доставка'
|
||||
let description = 'Доставка курьером до двери'
|
||||
|
||||
if (deliveryType === 'pickup') {
|
||||
name = 'Доставка в пункт выдачи (ПВЗ)'
|
||||
description = 'Получение в пункте выдачи заказов'
|
||||
deliveryCost = 0 // ПВЗ всегда бесплатно
|
||||
}
|
||||
|
||||
if (maxSupplierDeliveryDays > 0) {
|
||||
if (deliveryType === 'pickup') {
|
||||
description = `Доставка включает ${maxSupplierDeliveryDays} дн. поставки товара + доставку в ПВЗ`
|
||||
} else {
|
||||
description = `Доставка включает ${maxSupplierDeliveryDays} дн. поставки товара + доставку до двери`
|
||||
}
|
||||
}
|
||||
|
||||
const formattedDeliveryDate = deliveryDate.toLocaleDateString('ru-RU', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'long'
|
||||
})
|
||||
|
||||
return {
|
||||
id: offer.offer_id || `offer_${deliveryType}_${index}`,
|
||||
name,
|
||||
deliveryDate: formattedDeliveryDate,
|
||||
deliveryTime,
|
||||
cost: deliveryCost,
|
||||
description,
|
||||
type: deliveryType,
|
||||
expiresAt: offer.expires_at ? new Date(offer.expires_at).toISOString() : null
|
||||
}
|
||||
})
|
||||
|
||||
// Проверяем есть ли оффер для ПВЗ среди полученных от Яндекса
|
||||
const hasPickupOffer = formattedOffers.some(offer => offer.type === 'pickup')
|
||||
const hasCourierOffer = formattedOffers.some(offer => offer.type === 'courier')
|
||||
|
||||
// Добавляем стандартный ПВЗ оффер если его нет
|
||||
if (!hasPickupOffer) {
|
||||
console.log('📦 Добавляем стандартный ПВЗ оффер')
|
||||
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1 + maxSupplierDeliveryDays)
|
||||
|
||||
const standardPickupOffer = {
|
||||
id: 'standard_pickup',
|
||||
name: 'Доставка в пункт выдачи (ПВЗ)',
|
||||
deliveryDate: tomorrow.toLocaleDateString('ru-RU', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'long'
|
||||
}),
|
||||
deliveryTime: `С ${tomorrow.getDate()} ${tomorrow.toLocaleDateString('ru-RU', { month: 'long' })}`,
|
||||
cost: 0, // Самовывоз бесплатно
|
||||
description: maxSupplierDeliveryDays > 0
|
||||
? `Доставка включает ${maxSupplierDeliveryDays} дн. поставки товара + доставку в ПВЗ`
|
||||
: 'Получение в пункте выдачи заказов',
|
||||
type: 'pickup',
|
||||
expiresAt: null
|
||||
}
|
||||
|
||||
formattedOffers.push(standardPickupOffer)
|
||||
}
|
||||
|
||||
// Добавляем стандартный курьерский оффер если его нет
|
||||
if (!hasCourierOffer) {
|
||||
console.log('🚚 Добавляем стандартный курьерский оффер')
|
||||
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1 + maxSupplierDeliveryDays)
|
||||
|
||||
const standardCourierOffer = {
|
||||
id: 'standard_courier',
|
||||
name: 'Курьерская доставка',
|
||||
deliveryDate: tomorrow.toLocaleDateString('ru-RU', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'long'
|
||||
}),
|
||||
deliveryTime: '10:00-18:00',
|
||||
cost: 300, // Стандартная стоимость
|
||||
description: maxSupplierDeliveryDays > 0
|
||||
? `Доставка включает ${maxSupplierDeliveryDays} дн. поставки товара + доставку до двери`
|
||||
: 'Доставка курьером до двери',
|
||||
type: 'courier',
|
||||
expiresAt: null
|
||||
}
|
||||
|
||||
formattedOffers.push(standardCourierOffer)
|
||||
}
|
||||
|
||||
// Если совсем нет офферов, возвращаем полный набор стандартных
|
||||
if (formattedOffers.length === 0) {
|
||||
console.log('⚠️ Нет офферов от Яндекс Доставки, возвращаем полный стандартный набор')
|
||||
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1 + maxSupplierDeliveryDays)
|
||||
|
||||
const standardOffers = [
|
||||
{
|
||||
id: 'standard_courier',
|
||||
name: 'Курьерская доставка',
|
||||
deliveryDate: tomorrow.toLocaleDateString('ru-RU', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'long'
|
||||
}),
|
||||
deliveryTime: '10:00-18:00',
|
||||
cost: 300, // Стандартная стоимость
|
||||
description: maxSupplierDeliveryDays > 0
|
||||
? `Доставка включает ${maxSupplierDeliveryDays} дн. поставки товара + доставку до двери`
|
||||
: 'Доставка курьером до двери',
|
||||
type: 'courier',
|
||||
expiresAt: null
|
||||
},
|
||||
{
|
||||
id: 'standard_pickup',
|
||||
name: 'Доставка в пункт выдачи (ПВЗ)',
|
||||
deliveryDate: tomorrow.toLocaleDateString('ru-RU', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'long'
|
||||
}),
|
||||
deliveryTime: `С ${tomorrow.getDate()} ${tomorrow.toLocaleDateString('ru-RU', { month: 'long' })}`,
|
||||
cost: 0, // Самовывоз бесплатно
|
||||
description: maxSupplierDeliveryDays > 0
|
||||
? `Доставка включает ${maxSupplierDeliveryDays} дн. поставки товара + доставку в ПВЗ`
|
||||
: 'Получение в пункте выдачи заказов',
|
||||
type: 'pickup',
|
||||
expiresAt: null
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Получены стандартные варианты доставки',
|
||||
error: null,
|
||||
offers: standardOffers
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Офферы доставки успешно получены',
|
||||
error: null,
|
||||
offers: formattedOffers
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка получения офферов доставки:', error)
|
||||
|
||||
// В случае ошибки возвращаем стандартные варианты
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1 + maxSupplierDeliveryDays)
|
||||
|
||||
const fallbackOffers = [
|
||||
{
|
||||
id: 'fallback_courier',
|
||||
name: 'Курьерская доставка',
|
||||
deliveryDate: tomorrow.toLocaleDateString('ru-RU', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'long'
|
||||
}),
|
||||
deliveryTime: '10:00-18:00',
|
||||
cost: 300,
|
||||
description: maxSupplierDeliveryDays > 0
|
||||
? `Доставка включает ${maxSupplierDeliveryDays} дн. поставки товара + доставку до двери`
|
||||
: 'Доставка курьером до двери',
|
||||
type: 'courier',
|
||||
expiresAt: null
|
||||
},
|
||||
{
|
||||
id: 'fallback_pickup',
|
||||
name: 'Доставка в пункт выдачи (ПВЗ)',
|
||||
deliveryDate: tomorrow.toLocaleDateString('ru-RU', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'long'
|
||||
}),
|
||||
deliveryTime: `С ${tomorrow.getDate()} ${tomorrow.toLocaleDateString('ru-RU', { month: 'long' })}`,
|
||||
cost: 0, // Самовывоз бесплатно
|
||||
description: maxSupplierDeliveryDays > 0
|
||||
? `Доставка включает ${maxSupplierDeliveryDays} дн. поставки товара + доставку в ПВЗ`
|
||||
: 'Получение в пункте выдачи заказов',
|
||||
type: 'pickup',
|
||||
expiresAt: null
|
||||
}
|
||||
]
|
||||
|
||||
// Определяем сообщение в зависимости от типа ошибки
|
||||
let errorMessage = 'Временные проблемы с сервисом доставки'
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('Missing some required address details')) {
|
||||
errorMessage = 'Требуется уточнение адреса доставки'
|
||||
} else if (error.message.includes('no_delivery_options')) {
|
||||
errorMessage = 'Доставка в данный адрес временно недоступна'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true, // Меняем на true, так как мы предоставляем альтернативные варианты
|
||||
message: `${errorMessage}. Показаны стандартные варианты доставки.`,
|
||||
error: null, // Убираем детали ошибки API для пользователя
|
||||
offers: fallbackOffers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user