Files
sfera-new/src/graphql/resolvers.ts
Veronika Smirnova 0304f69410 Fix fulfillment consumables pricing architecture
- Add pricePerUnit field to Supply model for seller pricing
- Fix updateSupplyPrice mutation to update pricePerUnit only
- Separate purchase price (price) from selling price (pricePerUnit)
- Fix GraphQL mutations to include organization field (CREATE/UPDATE_LOGISTICS)
- Update GraphQL types to make Supply.price required again
- Add comprehensive pricing rules to rules-complete.md sections 11.7.5 and 18.8
- Fix supplies-tab.tsx to show debug info and handle user loading

Architecture changes:
• Supply.price = purchase price from supplier (immutable)
• Supply.pricePerUnit = selling price to sellers (mutable by fulfillment)
• Warehouse shows purchase price only (readonly)
• Services shows/edits selling price only

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 14:33:40 +03:00

8183 lines
272 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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