Files
sfera/src/graphql/resolvers.ts
2025-07-31 11:34:47 +03:00

6889 lines
212 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import 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;
notes?: string;
}
interface AuthTokenPayload {
userId: string;
phone: string;
}
// JWT утилиты
const generateToken = (payload: AuthTokenPayload): string => {
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: "30d" });
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const verifyToken = (token: string): AuthTokenPayload => {
try {
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
throw new GraphQLError("Недействительный токен", {
extensions: { code: "UNAUTHENTICATED" },
});
}
};
// Скалярный тип для JSON
const JSONScalar = new GraphQLScalarType({
name: "JSON",
description: "JSON custom scalar type",
serialize(value: unknown) {
return value; // значение отправляется клиенту
},
parseValue(value: unknown) {
return value; // значение получено от клиента
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value;
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value);
case Kind.OBJECT: {
const value = Object.create(null);
ast.fields.forEach((field) => {
value[field.name.value] = parseLiteral(field.value);
});
return value;
}
case Kind.LIST:
return ast.values.map(parseLiteral);
default:
return null;
}
},
});
// Скалярный тип для DateTime
const DateTimeScalar = new GraphQLScalarType({
name: "DateTime",
description: "DateTime custom scalar type",
serialize(value: unknown) {
if (value instanceof Date) {
return value.toISOString(); // значение отправляется клиенту как ISO строка
}
return value;
},
parseValue(value: unknown) {
if (typeof value === "string") {
return new Date(value); // значение получено от клиента, парсим как дату
}
return value;
},
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
return new Date(ast.value); // AST значение как дата
}
return null;
},
});
function parseLiteral(ast: unknown): unknown {
const astNode = ast as {
kind: string;
value?: unknown;
fields?: unknown[];
values?: unknown[];
};
switch (astNode.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return astNode.value;
case Kind.INT:
case Kind.FLOAT:
return parseFloat(astNode.value as string);
case Kind.OBJECT: {
const value = Object.create(null);
if (astNode.fields) {
astNode.fields.forEach((field: unknown) => {
const fieldNode = field as {
name: { value: string };
value: unknown;
};
value[fieldNode.name.value] = parseLiteral(fieldNode.value);
});
}
return value;
}
case Kind.LIST:
return (ast as { values: unknown[] }).values.map(parseLiteral);
default:
return null;
}
}
export const resolvers = {
JSON: JSONScalar,
DateTime: DateTimeScalar,
Query: {
me: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
return await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
});
},
organization: async (
_: unknown,
args: { id: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const organization = await prisma.organization.findUnique({
where: { id: args.id },
include: {
apiKeys: true,
users: true,
},
});
if (!organization) {
throw new GraphQLError("Организация не найдена");
}
// Проверяем, что пользователь имеет доступ к этой организации
const hasAccess = organization.users.some(
(user) => user.id === context.user!.id
);
if (!hasAccess) {
throw new GraphQLError("Нет доступа к этой организации", {
extensions: { code: "FORBIDDEN" },
});
}
return organization;
},
// Поиск организаций по типу для добавления в контрагенты
searchOrganizations: async (
_: unknown,
args: { type?: string; search?: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
// Получаем текущую организацию пользователя
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
// Получаем уже существующих контрагентов для добавления флага
const existingCounterparties = await prisma.counterparty.findMany({
where: { organizationId: currentUser.organization.id },
select: { counterpartyId: true },
});
const existingCounterpartyIds = existingCounterparties.map(
(c) => c.counterpartyId
);
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
const outgoingRequests = await prisma.counterpartyRequest.findMany({
where: {
senderId: currentUser.organization.id,
status: "PENDING",
},
select: { receiverId: true },
});
const outgoingRequestIds = outgoingRequests.map((r) => r.receiverId);
// Получаем входящие заявки для добавления флага hasIncomingRequest
const incomingRequests = await prisma.counterpartyRequest.findMany({
where: {
receiverId: currentUser.organization.id,
status: "PENDING",
},
select: { senderId: true },
});
const incomingRequestIds = incomingRequests.map((r) => r.senderId);
const where: Record<string, unknown> = {
// Больше не исключаем собственную организацию
};
if (args.type) {
where.type = args.type;
}
if (args.search) {
where.OR = [
{ name: { contains: args.search, mode: "insensitive" } },
{ fullName: { contains: args.search, mode: "insensitive" } },
{ inn: { contains: args.search } },
];
}
const organizations = await prisma.organization.findMany({
where,
take: 50, // Ограничиваем количество результатов
orderBy: { createdAt: "desc" },
include: {
users: true,
apiKeys: true,
},
});
// Добавляем флаги isCounterparty, isCurrentUser, hasOutgoingRequest и hasIncomingRequest к каждой организации
return organizations.map((org) => ({
...org,
isCounterparty: existingCounterpartyIds.includes(org.id),
isCurrentUser: org.id === currentUser.organization?.id,
hasOutgoingRequest: outgoingRequestIds.includes(org.id),
hasIncomingRequest: incomingRequestIds.includes(org.id),
}));
},
// Мои контрагенты
myCounterparties: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
const counterparties = await prisma.counterparty.findMany({
where: { organizationId: currentUser.organization.id },
include: {
counterparty: {
include: {
users: true,
apiKeys: true,
},
},
},
});
return counterparties.map((c) => c.counterparty);
},
// Поставщики поставок
supplySuppliers: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
const suppliers = await prisma.supplySupplier.findMany({
where: { organizationId: currentUser.organization.id },
orderBy: { createdAt: "desc" },
});
return suppliers;
},
// Логистика конкретной организации
organizationLogistics: async (
_: unknown,
args: { organizationId: string },
context: Context
) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
return await prisma.logistics.findMany({
where: { organizationId: args.organizationId },
orderBy: { createdAt: "desc" },
});
},
// Входящие заявки
incomingRequests: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
return await prisma.counterpartyRequest.findMany({
where: {
receiverId: currentUser.organization.id,
status: "PENDING",
},
include: {
sender: {
include: {
users: true,
apiKeys: true,
},
},
receiver: {
include: {
users: true,
apiKeys: true,
},
},
},
orderBy: { createdAt: "desc" },
});
},
// Исходящие заявки
outgoingRequests: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
return await prisma.counterpartyRequest.findMany({
where: {
senderId: currentUser.organization.id,
status: { in: ["PENDING", "REJECTED"] },
},
include: {
sender: {
include: {
users: true,
apiKeys: true,
},
},
receiver: {
include: {
users: true,
apiKeys: true,
},
},
},
orderBy: { createdAt: "desc" },
});
},
// Сообщения с контрагентом
messages: async (
_: unknown,
args: { counterpartyId: string; limit?: number; offset?: number },
context: Context
) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
const limit = args.limit || 50;
const offset = args.offset || 0;
const messages = await prisma.message.findMany({
where: {
OR: [
{
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.counterpartyId,
},
{
senderOrganizationId: args.counterpartyId,
receiverOrganizationId: currentUser.organization.id,
},
],
},
include: {
sender: true,
senderOrganization: {
include: {
users: true,
},
},
receiverOrganization: {
include: {
users: true,
},
},
},
orderBy: { createdAt: "asc" },
take: limit,
skip: offset,
});
return messages;
},
// Список чатов (последние сообщения с каждым контрагентом)
conversations: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
// Получаем всех контрагентов
const counterparties = await prisma.counterparty.findMany({
where: { organizationId: currentUser.organization.id },
include: {
counterparty: {
include: {
users: true,
},
},
},
});
// Для каждого контрагента получаем последнее сообщение и количество непрочитанных
const conversations = await Promise.all(
counterparties.map(async (cp) => {
const counterpartyId = cp.counterparty.id;
// Последнее сообщение с этим контрагентом
const lastMessage = await prisma.message.findFirst({
where: {
OR: [
{
senderOrganizationId: currentUser.organization!.id,
receiverOrganizationId: counterpartyId,
},
{
senderOrganizationId: counterpartyId,
receiverOrganizationId: currentUser.organization!.id,
},
],
},
include: {
sender: true,
senderOrganization: {
include: {
users: true,
},
},
receiverOrganization: {
include: {
users: true,
},
},
},
orderBy: { createdAt: "desc" },
});
// Количество непрочитанных сообщений от этого контрагента
const unreadCount = await prisma.message.count({
where: {
senderOrganizationId: counterpartyId,
receiverOrganizationId: currentUser.organization!.id,
isRead: false,
},
});
// Если есть сообщения с этим контрагентом, включаем его в список
if (lastMessage) {
return {
id: `${currentUser.organization!.id}-${counterpartyId}`,
counterparty: cp.counterparty,
lastMessage,
unreadCount,
updatedAt: lastMessage.createdAt,
};
}
return null;
})
);
// Фильтруем null значения и сортируем по времени последнего сообщения
return conversations
.filter((conv) => conv !== null)
.sort(
(a, b) =>
new Date(b!.updatedAt).getTime() - new Date(a!.updatedAt).getTime()
);
},
// Мои услуги
myServices: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError("Услуги доступны только для фулфилмент центров");
}
return await prisma.service.findMany({
where: { organizationId: currentUser.organization.id },
include: { organization: true },
orderBy: { createdAt: "desc" },
});
},
// Расходники селлеров (материалы клиентов на складе фулфилмента)
mySupplies: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
// Получаем заказы поставок, где фулфилмент является получателем,
// но НЕ создателем (т.е. селлеры заказали расходники для фулфилмента)
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 existingSupplies = await prisma.supply.findMany({
where: { organizationId: currentUser.organization.id },
include: { organization: true },
orderBy: { createdAt: "desc" },
});
// Логирование для отладки
console.log("🔥🔥🔥 SELLER SUPPLIES RESOLVER CALLED 🔥🔥🔥");
console.log("📊 Расходники селлеров:", {
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
existingSuppliesCount: existingSupplies.length,
sellerOrdersCount: sellerSupplyOrders.length,
sellerOrders: sellerSupplyOrders.map((o) => ({
id: o.id,
sellerName: o.organization.name,
supplierName: o.partner.name,
status: o.status,
itemsCount: o.items.length,
})),
});
// Возвращаем только расходники селлеров из таблицы supply
// TODO: В будущем можно добавить фильтрацию по источнику заказа
return existingSupplies;
},
// Расходники фулфилмента (материалы для работы фулфилмента)
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"
? "in-transit"
: order.status === "CONFIRMED"
? "in-transit"
: order.status === "IN_TRANSIT"
? "in-transit"
: "available",
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 }, // Заказы где организация - получатель (фулфилмент)
],
},
include: {
partner: {
include: {
users: true,
},
},
organization: {
include: {
users: true,
},
},
fulfillmentCenter: {
include: {
users: 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",
"SUPPLIER_APPROVED",
"LOGISTICS_CONFIRMED",
"IN_TRANSIT",
"SHIPPED",
],
}, // Активные статусы
},
});
// Расходники селлеров (созданные другими для нас) - требуют подтверждения получения
const sellerSupplyOrders = await prisma.supplyOrder.count({
where: {
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
organizationId: { not: currentUser.organization.id }, // Создали НЕ мы
status: { in: ["IN_TRANSIT", "SHIPPED"] }, // В пути или отправлено - нужно подтвердить получение
},
});
// 🔔 ВХОДЯЩИЕ ЗАКАЗЫ ДЛЯ ПОСТАВЩИКОВ (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,
};
},
// Логистика организации
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<string, unknown> = {
isActive: true, // Показываем только активные товары
// Показываем и товары, и расходники поставщиков
organization: {
type: "WHOLESALE", // Только товары поставщиков
},
};
if (args.search) {
where.OR = [
{ name: { contains: args.search, mode: "insensitive" } },
{ article: { contains: args.search, mode: "insensitive" } },
{ description: { contains: args.search, mode: "insensitive" } },
{ brand: { contains: args.search, mode: "insensitive" } },
];
}
if (args.category) {
where.categoryId = args.category;
}
const products = await prisma.product.findMany({
where,
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
orderBy: { createdAt: "desc" },
take: 100, // Ограничиваем количество результатов
});
console.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; // Дополнительные заметки к заказу
};
},
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,
status: initialStatus,
items: {
create: orderItems,
},
},
include: {
partner: {
include: {
users: true,
},
},
organization: {
include: {
users: true,
},
},
fulfillmentCenter: {
include: {
users: true,
},
},
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
},
},
});
// Создаем расходники на основе заказанных товаров
// Расходники создаются в организации получателя (фулфилмент-центре)
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: "in-transit", // Статус "в пути" так как заказ только создан
date: new Date(args.input.deliveryDate),
supplier: partner.name || partner.fullName || "Не указан",
minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток
currentStock: 0, // Пока товар не пришел
// Расходники создаются в организации получателя (фулфилмент-центре)
organizationId: fulfillmentCenterId || currentUser.organization!.id,
};
});
// Создаем расходники
await prisma.supply.createMany({
data: suppliesData,
});
// 🔔 ОТПРАВЛЯЕМ УВЕДОМЛЕНИЕ ПОСТАВЩИКУ О НОВОМ ЗАКАЗЕ
try {
const orderSummary = args.input.items
.map((item) => {
const product = products.find((p) => p.id === item.productId)!;
return `${product.name} - ${item.quantity} шт.`;
})
.join(", ");
const notificationMessage = `🔔 Новый заказ поставки от ${
currentUser.organization.name || currentUser.organization.fullName
}!\n\nТовары: ${orderSummary}\ата доставки: ${new Date(
args.input.deliveryDate
).toLocaleDateString(
"ru-RU"
)}\nОбщая сумма: ${totalAmount.toLocaleString(
"ru-RU"
)}\n\ожалуйста, подтвердите заказ в разделе "Поставки".`;
await prisma.message.create({
data: {
content: notificationMessage,
type: "TEXT",
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.input.partnerId,
},
});
console.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;
quantity: 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,
quantity: args.input.quantity,
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;
quantity: 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,
quantity: args.input.quantity,
...(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: "Ошибка при обновлении товара",
};
}
},
// Удалить товар
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,
notes: args.input.notes,
},
update: {
status: args.input.status,
hoursWorked: args.input.hoursWorked,
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"
| "SUPPLIER_APPROVED"
| "LOGISTICS_CONFIRMED"
| "IN_TRANSIT"
| "SHIPPED"
| "DELIVERED"
| "CANCELLED";
},
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,
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,
},
},
},
},
},
});
// Если статус изменился на DELIVERED, обновляем склад фулфилмента
if (args.status === "DELIVERED" && existingOrder.fulfillmentCenterId) {
console.log("🚚 Обновляем склад фулфилмента:", {
fulfillmentCenterId: existingOrder.fulfillmentCenterId,
itemsCount: existingOrder.items.length,
items: existingOrder.items.map((item) => ({
productName: item.product.name,
quantity: item.quantity,
})),
});
// Обновляем расходники фулфилмента
for (const item of existingOrder.items) {
console.log("📦 Обрабатываем товар:", {
productName: item.product.name,
quantity: item.quantity,
fulfillmentCenterId: existingOrder.fulfillmentCenterId,
});
// Ищем существующий расходник
const existingSupply = await prisma.supply.findFirst({
where: {
name: item.product.name,
organizationId: existingOrder.fulfillmentCenterId,
},
});
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: "available", // Меняем статус на "доступен"
},
});
} else {
console.log(" Создаем новый расходник:", {
name: item.product.name,
quantity: item.quantity,
organizationId: existingOrder.fulfillmentCenterId,
});
// Создаем новый расходник
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: "available",
date: new Date(),
supplier:
existingOrder.partner.name ||
existingOrder.partner.fullName ||
"Не указан",
minStock: Math.round(item.quantity * 0.1),
currentStock: item.quantity,
organizationId: existingOrder.fulfillmentCenterId,
},
});
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: "Ошибка при обновлении статуса заказа поставки",
};
}
},
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,
};