Files
sfera/src/graphql/resolvers.ts

4565 lines
134 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 { Prisma } from "@prisma/client";
// Сервисы
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);
},
// Входящие заявки
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("У пользователя нет организации");
}
// TODO: Здесь будет логика получения списка чатов
// Пока возвращаем пустой массив, так как таблица сообщений еще не создана
return [];
},
// Мои услуги
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("У пользователя нет организации");
}
return await prisma.supply.findMany({
where: { organizationId: currentUser.organization.id },
include: { organization: true },
orderBy: { createdAt: "desc" },
});
},
// Логистика организации
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" },
});
},
// Мои товары (для оптовиков)
myProducts: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" },
});
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
});
if (!currentUser?.organization) {
throw new GraphQLError("У пользователя нет организации");
}
// Проверяем, что это оптовик
if (currentUser.organization.type !== "WHOLESALE") {
throw new GraphQLError("Товары доступны только для оптовиков");
}
return await prisma.product.findMany({
where: { organizationId: currentUser.organization.id },
include: {
category: true,
organization: true,
},
orderBy: { createdAt: "desc" },
});
},
// Все товары всех оптовиков для маркета
allProducts: async (
_: unknown,
args: { search?: string; category?: string },
context: Context
) => {
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;
}
return await prisma.product.findMany({
where,
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
orderBy: { createdAt: "desc" },
take: 100, // Ограничиваем количество результатов
});
},
// Все категории
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" },
});
},
// Корзина пользователя
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;
// Валидируем API ключ
const validationResult = await marketplaceService.validateApiKey(
marketplace,
apiKey,
clientId
);
if (!validationResult.isValid) {
return {
success: false,
message: validationResult.message,
};
}
// Если это только валидация, возвращаем результат без сохранения
if (validateOnly) {
return {
success: true,
message: "API ключ действителен",
apiKey: {
id: "validate-only",
marketplace,
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" },
});
}
// TODO: Здесь будет логика обновления статуса сообщений
// Пока возвращаем успешный ответ
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;
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: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса
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;
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: 0, // Временно устанавливаем 0, так как поле убрано из интерфейса
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;
}
},
// Создать заказ поставки расходников
createSupplyOrder: async (
_: unknown,
args: {
input: {
partnerId: string;
deliveryDate: string;
items: Array<{ 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("У пользователя нет организации");
}
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== "FULFILLMENT") {
throw new GraphQLError(
"Заказы поставок доступны только для фулфилмент центров"
);
}
// Проверяем, что партнер существует и является оптовиком
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 {
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,
items: {
create: orderItems,
},
},
include: {
partner: {
include: {
users: true,
},
},
organization: {
include: {
users: true,
},
},
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
},
},
});
return {
success: true,
message: "Заказ поставки создан успешно",
order: supplyOrder,
};
} 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;
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("У пользователя нет организации");
}
// Проверяем, что это оптовик
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 {
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,
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 || [],
mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true,
organizationId: currentUser.organization.id,
},
include: {
category: true,
organization: true,
},
});
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;
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,
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 || [],
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;
}
},
},
// Резолверы типов
Organization: {
users: async (parent: { id: string; users?: unknown[] }) => {
// Если пользователи уже загружены через include, возвращаем их
if (parent.users) {
return parent.users;
}
// Иначе загружаем отдельно
return await prisma.user.findMany({
where: { organizationId: parent.id },
});
},
},
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;
},
},
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 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,
...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;
},
};
// Добавляем админ запросы и мутации к основным резолверам
resolvers.Query = {
...resolvers.Query,
...adminQueries,
};
resolvers.Mutation = {
...resolvers.Mutation,
...adminMutations,
};