import jwt from "jsonwebtoken"; import bcrypt from "bcryptjs"; import { GraphQLError } from "graphql"; import { GraphQLScalarType, Kind } from "graphql"; import { prisma } from "@/lib/prisma"; import { SmsService } from "@/services/sms-service"; import { DaDataService } from "@/services/dadata-service"; import { MarketplaceService } from "@/services/marketplace-service"; import { WildberriesService } from "@/services/wildberries-service"; import { Prisma } from "@prisma/client"; 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 = { // Больше не исключаем собственную организацию }; 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("У пользователя нет организации"); } // Получаем заказы поставок, где фулфилмент является получателем, // но НЕ создателем (т.е. селлеры заказали расходники для фулфилмента) const sellerSupplyOrders = await prisma.supplyOrder.findMany({ where: { fulfillmentCenterId: currentUser.organization.id, // Получатель - мы organizationId: { not: currentUser.organization.id }, // Создатель - НЕ мы status: "DELIVERED", // Только доставленные }, include: { organization: true, partner: true, items: { include: { product: { include: { category: true, }, }, }, }, }, }); // Получаем ВСЕ расходники из таблицы supply для фулфилмента const allSupplies = await prisma.supply.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true }, orderBy: { createdAt: "desc" }, }); // Получаем все заказы фулфилмента для себя (чтобы исключить их расходники) const fulfillmentOwnOrders = await prisma.supplyOrder.findMany({ where: { organizationId: currentUser.organization.id, // Созданы фулфилментом fulfillmentCenterId: currentUser.organization.id, // Для себя status: "DELIVERED", }, include: { items: { include: { product: true, }, }, }, }); // Создаем набор названий товаров из заказов фулфилмента для себя const fulfillmentProductNames = new Set( fulfillmentOwnOrders.flatMap((order) => order.items.map((item) => item.product.name) ) ); // Фильтруем расходники: исключаем те, что созданы заказами фулфилмента для себя const sellerSupplies = allSupplies.filter((supply) => { // Если расходник соответствует товару из заказа фулфилмента для себя, // то это расходник фулфилмента, а не селлера return !fulfillmentProductNames.has(supply.name); }); // Логирование для отладки console.log("🔥🔥🔥 SELLER SUPPLIES RESOLVER CALLED 🔥🔥🔥"); console.log("📊 Расходники селлеров:", { organizationId: currentUser.organization.id, organizationType: currentUser.organization.type, allSuppliesCount: allSupplies.length, fulfillmentOwnOrdersCount: fulfillmentOwnOrders.length, fulfillmentProductNames: Array.from(fulfillmentProductNames), filteredSellerSuppliesCount: sellerSupplies.length, sellerOrdersCount: sellerSupplyOrders.length, }); // Возвращаем только расходники селлеров (исключая расходники фулфилмента) return sellerSupplies; }, // Расходники фулфилмента (материалы для работы фулфилмента) myFulfillmentSupplies: 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("У пользователя нет организации"); } // TypeScript assertion - мы знаем что organization не null после проверки выше const organization = currentUser.organization; // Получаем заказы поставок, созданные этим фулфилмент-центром для себя const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({ where: { organizationId: organization.id, // Создали мы fulfillmentCenterId: organization.id, // Получатель - мы status: { in: ["PENDING", "CONFIRMED", "IN_TRANSIT", "DELIVERED"], // Все статусы }, }, include: { partner: true, items: { include: { product: { include: { category: true, }, }, }, }, }, orderBy: { createdAt: "desc" }, }); // Преобразуем заказы поставок в формат supply для единообразия const fulfillmentSupplies = fulfillmentSupplyOrders.flatMap((order) => order.items.map((item) => ({ id: `fulfillment-order-${order.id}-${item.id}`, name: item.product.name, description: item.product.description || `Расходники от ${order.partner.name}`, price: item.price, quantity: item.quantity, unit: "шт", category: item.product.category?.name || "Расходники фулфилмента", status: order.status === "PENDING" ? "planned" : order.status === "CONFIRMED" ? "confirmed" : order.status === "IN_TRANSIT" ? "in-transit" : order.status === "DELIVERED" ? "in-stock" : "planned", date: order.createdAt, supplier: order.partner.name || order.partner.fullName || "Не указан", minStock: Math.round(item.quantity * 0.1), currentStock: order.status === "DELIVERED" ? item.quantity : 0, usedStock: 0, // TODO: Подсчитывать реальное использование imageUrl: null, createdAt: order.createdAt, updatedAt: order.updatedAt, organizationId: organization.id, organization: organization, shippedQuantity: 0, })) ); // Логирование для отладки console.log("🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED 🔥🔥🔥"); console.log("📊 Расходники фулфилмента:", { organizationId: organization.id, organizationType: organization.type, fulfillmentOrdersCount: fulfillmentSupplyOrders.length, fulfillmentSuppliesCount: fulfillmentSupplies.length, fulfillmentOrders: fulfillmentSupplyOrders.map((o) => ({ id: o.id, supplierName: o.partner.name, status: o.status, itemsCount: o.items.length, })), }); return fulfillmentSupplies; }, // Заказы поставок расходников 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("У пользователя нет организации"); } // Возвращаем заказы где текущая организация является заказчиком, поставщиком, получателем или логистическим партнером return 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" }, }); }, // Счетчик поставок, требующих одобрения 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_TRANSIT", // В пути - нужно подтвердить получение }, }); // 🔔 ВХОДЯЩИЕ ЗАКАЗЫ ДЛЯ ПОСТАВЩИКОВ (WHOLESALE) - требуют подтверждения const incomingSupplierOrders = await prisma.supplyOrder.count({ where: { partnerId: currentUser.organization.id, // Мы - поставщик status: "PENDING", // Ожидает подтверждения от поставщика }, }); // Общий счетчик поставок const pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders + incomingSupplierOrders; // Считаем входящие заявки на партнерство со статусом PENDING const pendingIncomingRequests = await prisma.counterpartyRequest.count({ where: { receiverId: currentUser.organization.id, status: "PENDING", }, }); return { supplyOrders: pendingSupplyOrders, ourSupplyOrders: ourSupplyOrders, // Расходники фулфилмента sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров incomingSupplierOrders: incomingSupplierOrders, // 🔔 Входящие заказы для поставщиков incomingRequests: pendingIncomingRequests, total: pendingSupplyOrders + pendingIncomingRequests, }; }, // Статистика склада фулфилмента с изменениями за сутки fulfillmentWarehouseStats: async ( _: unknown, __: unknown, context: Context ) => { console.log("🔥 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.log( `🏢 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.log(`📦 ALL DELIVERED ORDERS: ${allSupplyOrders.length}`); allSupplyOrders.forEach((order) => { console.log( ` 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.log( `🛒 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.log( `🏭 FULFILLMENT SUPPLY ORDERS: ${fulfillmentSupplyOrders.length}` ); // Подсчитываем количество из таблицы Supply (актуальные остатки на складе фулфилмента) const fulfillmentSuppliesFromWarehouse = await prisma.supply.findMany({ where: { organizationId: organizationId, // Склад фулфилмента }, }); const fulfillmentSuppliesCount = fulfillmentSuppliesFromWarehouse.reduce( (sum, supply) => sum + (supply.currentStock || 0), 0 ); console.log( `🔥 FULFILLMENT SUPPLIES DEBUG: organizationId=${organizationId}, ordersCount=${fulfillmentSupplyOrders.length}, warehouseCount=${fulfillmentSuppliesFromWarehouse.length}, totalStock=${fulfillmentSuppliesCount}` ); console.log( `📦 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.log( `📊 FULFILLMENT SUPPLIES RECEIVED TODAY (ПРИБЫЛО): ${fulfillmentSuppliesReceivedToday.length} orders, ${fulfillmentSuppliesChangeToday} items` ); // Расходники селлеров - получаем из заказов от селлеров (расходники = CONSUMABLE) // Согласно правилам: селлеры заказывают расходники у поставщиков и доставляют на склад фулфилмента const sellerSuppliesCount = sellerDeliveredOrders.reduce( (sum, order) => sum + order.items.reduce( (itemSum, item) => itemSum + (item.product.type === "CONSUMABLE" ? item.quantity : 0), 0 ), 0 ); console.log( `💼 SELLER SUPPLIES DEBUG: totalCount=${sellerSuppliesCount} (from delivered orders)` ); // Изменения расходников селлеров за сутки - используем уже полученные данные const sellerSuppliesChangeToday = recentSellerDeliveredOrders.reduce( (sum, order) => sum + order.items.reduce( (itemSum, item) => itemSum + (item.product.type === "CONSUMABLE" ? item.quantity : 0), 0 ), 0 ); console.log( `📊 SELLER SUPPLIES RECEIVED TODAY: ${recentSellerDeliveredOrders.length} orders, ${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.log( `🏁 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" }, }); }, // Мои поставки 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" }, }); }, // Мои товары и расходники (для поставщиков) myProducts: async (_: unknown, __: unknown, context: Context) => { console.log("🔍 MY_PRODUCTS RESOLVER - ВЫЗВАН:", { hasUser: !!context.user, userId: context.user?.id, timestamp: new Date().toISOString(), }); if (!context.user) { throw new GraphQLError("Требуется авторизация", { extensions: { code: "UNAUTHENTICATED" }, }); } const currentUser = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }); console.log("👤 ПОЛЬЗОВАТЕЛЬ НАЙДЕН:", { userId: currentUser?.id, hasOrganization: !!currentUser?.organization, organizationType: currentUser?.organization?.type, organizationName: currentUser?.organization?.name, }); if (!currentUser?.organization) { throw new GraphQLError("У пользователя нет организации"); } // Проверяем, что это поставщик if (currentUser.organization.type !== "WHOLESALE") { console.log("❌ ДОСТУП ЗАПРЕЩЕН - НЕ ПОСТАВЩИК:", { actualType: currentUser.organization.type, requiredType: "WHOLESALE", }); throw new GraphQLError("Товары доступны только для поставщиков"); } const products = await prisma.product.findMany({ where: { organizationId: currentUser.organization.id, // Показываем и товары, и расходники поставщика }, include: { category: true, organization: true, }, orderBy: { createdAt: "desc" }, }); console.log("🔥 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.log("🔍 Резолвер 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.log( `📦 Заказ от селлера ${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.log( `🚫 Исключен расходник из основного склада фулфилмента:`, { name: item.product.name, type: item.product.type, orderId: order.id, } ); } } } console.log( "✅ Итого товаров на складе фулфилмента (из доставленных заказов):", allProducts.length ); return allProducts; }, // Все товары и расходники поставщиков для маркета allProducts: async ( _: unknown, args: { search?: string; category?: string }, context: Context ) => { console.log("🛍️ 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 = { 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.log("🔥 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; }, // Все категории 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) => { 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 employees = await prisma.employee.findMany({ where: { organizationId: currentUser.organization.id }, include: { organization: true, }, orderBy: { createdAt: "desc" }, }); return employees; }, // Получение сотрудника по 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.log( "verifySmsCode - Generated token:", token ? `${token.substring(0, 20)}...` : "No token" ); console.log("verifySmsCode - Full token:", token); console.log("verifySmsCode - User object:", { id: user.id, phone: user.phone, }); const result = { success: true, message: "Авторизация успешна", token, user, }; console.log("verifySmsCode - Returning result:", { success: result.success, hasToken: !!result.token, hasUser: !!result.user, message: result.message, tokenPreview: result.token ? `${result.token.substring(0, 20)}...` : "No token in result", }); return result; }, verifyInn: async (_: unknown, args: { inn: string }) => { // Валидируем ИНН if (!dadataService.validateInn(args.inn)) { return { success: false, message: "Неверный формат ИНН", }; } // Получаем данные организации из DaData const organizationData = await dadataService.getOrganizationByInn( args.inn ); if (!organizationData) { return { success: false, message: "Организация с указанным ИНН не найдена", }; } return { success: true, message: "ИНН найден", organization: { name: organizationData.name, fullName: organizationData.fullName, address: organizationData.address, isActive: organizationData.isActive, }, }; }, registerFulfillmentOrganization: async ( _: unknown, args: { input: { phone: string; inn: string; 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.log(`🔍 Validating ${marketplace} API key:`, { keyLength: apiKey.length, keyPreview: apiKey.substring(0, 20) + "...", validateOnly, }); // Валидируем API ключ const validationResult = await marketplaceService.validateApiKey( marketplace, apiKey, clientId ); console.log(`✅ Validation result for ${marketplace}:`, validationResult); if (!validationResult.isValid) { console.log( `❌ 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; }; }, 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; } = {}; // Название организации больше не обновляется через профиль // Для селлеров устанавливается при регистрации, для остальных - при смене ИНН // Обновляем контактные данные в JSON поле phones if (input.orgPhone) { updateData.phones = [{ value: input.orgPhone, type: "main" }]; } // Обновляем email в JSON поле emails if (input.email) { updateData.emails = [{ value: input.email, type: "main" }]; } // Сохраняем дополнительные контакты в custom полях // Пока добавим их как дополнительные JSON поля const customContacts: { managerName?: string; telegram?: string; whatsapp?: string; bankDetails?: { bankName?: string; bik?: string; accountNumber?: string; corrAccount?: string; }; } = {}; // 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; } }, // Создать расходник createSupply: async ( _: unknown, args: { input: { name: string; description?: string; price: number; quantity: number; unit: string; category: string; status: string; date: string; supplier: string; minStock: number; currentStock: 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 supply = await prisma.supply.create({ data: { name: args.input.name, description: args.input.description, price: args.input.price, quantity: args.input.quantity, unit: args.input.unit, category: args.input.category, status: args.input.status, date: new Date(args.input.date), supplier: args.input.supplier, minStock: args.input.minStock, currentStock: args.input.currentStock, imageUrl: args.input.imageUrl, organizationId: currentUser.organization.id, }, include: { organization: true }, }); return { success: true, message: "Расходник успешно создан", supply, }; } catch (error) { console.error("Error creating supply:", error); return { success: false, message: "Ошибка при создании расходника", }; } }, // Обновить расходник updateSupply: async ( _: unknown, args: { id: string; input: { name: string; description?: string; price: number; quantity: number; unit: string; category: string; status: string; date: string; supplier: string; minStock: number; currentStock: 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 existingSupply = await prisma.supply.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); if (!existingSupply) { throw new GraphQLError("Расходник не найден или нет доступа"); } try { const supply = await prisma.supply.update({ where: { id: args.id }, data: { name: args.input.name, description: args.input.description, price: args.input.price, quantity: args.input.quantity, unit: args.input.unit, category: args.input.category, status: args.input.status, date: new Date(args.input.date), supplier: args.input.supplier, minStock: args.input.minStock, currentStock: args.input.currentStock, imageUrl: args.input.imageUrl, }, include: { organization: true }, }); return { success: true, message: "Расходник успешно обновлен", supply, }; } catch (error) { console.error("Error updating supply:", error); return { success: false, message: "Ошибка при обновлении расходника", }; } }, // Удалить расходник deleteSupply: 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 existingSupply = await prisma.supply.findFirst({ where: { id: args.id, organizationId: currentUser.organization.id, }, }); if (!existingSupply) { throw new GraphQLError("Расходник не найден или нет доступа"); } try { await prisma.supply.delete({ where: { id: args.id }, }); return true; } catch (error) { console.error("Error deleting supply:", error); return false; } }, // Использовать расходники фулфилмента 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.log("🔧 Использованы расходники фулфилмента:", { 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 }, }); 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 supplyOrder = await prisma.supplyOrder.create({ data: { partnerId: args.input.partnerId, deliveryDate: new Date(args.input.deliveryDate), totalAmount: new Prisma.Decimal(totalAmount), totalItems: totalItems, organizationId: currentUser.organization.id, fulfillmentCenterId: fulfillmentCenterId, logisticsPartnerId: args.input.logisticsPartnerId, consumableType: args.input.consumableType, // Классификация расходников status: initialStatus, items: { create: orderItems, }, }, 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.log( `📦 Зарезервированы товары для заказа ${supplyOrder.id}:`, args.input.items .map((item) => `${item.productId}: +${item.quantity} шт.`) .join(", ") ); // Создаем расходники на основе заказанных товаров // Расходники создаются в организации получателя (фулфилмент-центре) const suppliesData = args.input.items.map((item) => { const product = products.find((p) => p.id === item.productId)!; const productWithCategory = supplyOrder.items.find( (orderItem: { productId: string; product: { category?: { name: string } | null }; }) => orderItem.productId === item.productId )?.product; return { name: product.name, description: product.description || `Заказано у ${partner.name}`, price: product.price, quantity: item.quantity, unit: "шт", category: productWithCategory?.category?.name || "Расходники", status: "planned", // Статус "запланировано" (ожидает одобрения поставщиком) date: new Date(args.input.deliveryDate), supplier: partner.name || partner.fullName || "Не указан", minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток currentStock: 0, // Пока товар не пришел // Расходники создаются в организации получателя (фулфилмент-центре) organizationId: fulfillmentCenterId || currentUser.organization!.id, }; }); // Создаем расходники await prisma.supply.createMany({ data: suppliesData, }); // 🔔 ОТПРАВЛЯЕМ УВЕДОМЛЕНИЕ ПОСТАВЩИКУ О НОВОМ ЗАКАЗЕ try { const orderSummary = args.input.items .map((item) => { const product = products.find((p) => p.id === item.productId)!; return `${product.name} - ${item.quantity} шт.`; }) .join(", "); const notificationMessage = `🔔 Новый заказ поставки от ${ currentUser.organization.name || currentUser.organization.fullName }!\n\nТовары: ${orderSummary}\nДата доставки: ${new Date( args.input.deliveryDate ).toLocaleDateString( "ru-RU" )}\nОбщая сумма: ${totalAmount.toLocaleString( "ru-RU" )} ₽\n\nПожалуйста, подтвердите заказ в разделе "Поставки".`; await prisma.message.create({ data: { content: notificationMessage, type: "TEXT", senderId: context.user.id, senderOrganizationId: currentUser.organization.id, receiverOrganizationId: args.input.partnerId, }, }); console.log(`✅ Уведомление отправлено поставщику ${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.log("🆕 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.log("🛍️ СОЗДАНИЕ ТОВАРА - НАЧАЛО:", { 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.log("✅ ТОВАР УСПЕШНО СОЗДАН:", { 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.log( `📦 Зарезервировано ${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.log( `🔄 Освобожден резерв ${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.log( `🚚 Обновлен статус "в пути" для товара ${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.log("Создание поставки 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.log( `[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.log( `[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.log("✅ Статусы расходников обновлены на '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.log("✅ Статусы расходников обновлены на 'in-transit'"); } // Если статус изменился на DELIVERED, обновляем склад if (args.status === "DELIVERED") { console.log("🚚 Обновляем склад организации:", { 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) { // Согласно правилам: Основные значения = Предыдущие остатки + Прибыло - Убыло 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, // Синхронизируем оба поля для совместимости // Обновляем дополнительные значения inTransit: Math.max( (product.inTransit || 0) - item.quantity, 0 ), sold: (product.sold || 0) + item.quantity, }, }); console.log( `✅ Товар поставщика "${product.name}" обновлен: доставлено ${item.quantity} единиц (остаток: ${currentStock} -> ${newStock})` ); } } // Обновляем расходники for (const item of existingOrder.items) { console.log("📦 Обрабатываем товар:", { productName: item.product.name, quantity: item.quantity, targetOrganizationId, }); // Ищем существующий расходник в правильной организации const existingSupply = await prisma.supply.findFirst({ where: { name: item.product.name, organizationId: targetOrganizationId, }, }); console.log("🔍 Найден существующий расходник:", !!existingSupply); if (existingSupply) { console.log("📈 Обновляем существующий расходник:", { 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.log("➕ Создаем новый расходник:", { 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.log("✅ Создан новый расходник:", { id: newSupply.id, name: newSupply.name, currentStock: newSupply.currentStock, }); } } console.log("🎉 Склад организации успешно обновлен!"); } return { success: true, message: `Статус заказа поставки обновлен на "${args.status}"`, order: updatedOrder, }; } catch (error) { console.error("Error updating supply order status:", error); return { success: false, 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.log( `[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.log( `📦 Товар "${product.name}" зарезервирован: ${item.quantity} единиц` ); console.log( ` 📊 Остаток: ${currentStock} -> ${newStock} (уменьшен на ${item.quantity})` ); console.log( ` 📋 Заказано: ${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.log( `[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.log( `🔄 Восстановлены остатки товара "${ product.name }": ${currentStock} -> ${restoredStock}, ordered: ${ product.ordered } -> ${Math.max((product.ordered || 0) - item.quantity, 0)}` ); } } console.log( `📦 Снята резервация при отклонении заказа ${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.log( `🚚 Товар "${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, status: "SUPPLIER_APPROVED", }, }); 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, status: "SUPPLIER_APPROVED", }, }); 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: 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.log("🔄 Начинаем синхронизацию остатков поставщика..."); for (const item of existingOrder.items) { const product = await prisma.product.findUnique({ where: { id: item.product.id }, }); if (product) { // Согласно правилам: Основные значения = Предыдущие остатки + Прибыло - Убыло 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, // Синхронизируем оба поля для совместимости // Обновляем дополнительные значения inTransit: Math.max( (product.inTransit || 0) - item.quantity, 0 ), sold: (product.sold || 0) + item.quantity, }, }); console.log( `✅ Товар поставщика "${product.name}" обновлен: доставлено ${item.quantity} единиц` ); console.log( ` 📊 Остатки: ${currentStock} -> ${newStock} (УБЫЛО: ${item.quantity})` ); console.log( ` 🚚 В пути: ${product.inTransit || 0} -> ${Math.max( (product.inTransit || 0) - item.quantity, 0 )}` ); console.log( ` 💰 Продано: ${product.sold || 0} -> ${ (product.sold || 0) + item.quantity }` ); } } // Обновляем склад фулфилмента console.log("📦 Обновляем склад фулфилмента..."); for (const item of existingOrder.items) { const existingSupply = await prisma.supply.findFirst({ where: { organizationId: currentUser.organization.id, name: item.product.name, }, }); 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.log( `📈 Обновлен существующий расходник фулфилмента "${ item.product.name }": ${existingSupply.currentStock} -> ${ existingSupply.currentStock + item.quantity }` ); } else { await prisma.supply.create({ data: { name: item.product.name, description: 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 || "Поставщик", organizationId: currentUser.organization.id, }, }); console.log( `➕ Создан новый расходник фулфилмента "${item.product.name}": ${item.quantity} единиц` ); } } console.log("🎉 Синхронизация склада завершена успешно!"); 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 }, 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: { 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.log("✅ 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.log("✅ 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.log("✅ 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, }, }; } }, }; // Резолверы для внешней рекламы 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, };