7669 lines
239 KiB
TypeScript
7669 lines
239 KiB
TypeScript
import jwt from "jsonwebtoken";
|
||
import bcrypt from "bcryptjs";
|
||
import { GraphQLError } from "graphql";
|
||
import { GraphQLScalarType, Kind } from "graphql";
|
||
import { prisma } from "@/lib/prisma";
|
||
import { SmsService } from "@/services/sms-service";
|
||
import { DaDataService } from "@/services/dadata-service";
|
||
import { MarketplaceService } from "@/services/marketplace-service";
|
||
import { WildberriesService } from "@/services/wildberries-service";
|
||
import { Prisma } from "@prisma/client";
|
||
import "@/lib/seed-init"; // Автоматическая инициализация БД
|
||
|
||
// Сервисы
|
||
const smsService = new SmsService();
|
||
const dadataService = new DaDataService();
|
||
const marketplaceService = new MarketplaceService();
|
||
|
||
// Интерфейсы для типизации
|
||
interface Context {
|
||
user?: {
|
||
id: string;
|
||
phone: string;
|
||
};
|
||
admin?: {
|
||
id: string;
|
||
username: string;
|
||
};
|
||
}
|
||
|
||
interface CreateEmployeeInput {
|
||
firstName: string;
|
||
lastName: string;
|
||
middleName?: string;
|
||
birthDate?: string;
|
||
avatar?: string;
|
||
passportPhoto?: string;
|
||
passportSeries?: string;
|
||
passportNumber?: string;
|
||
passportIssued?: string;
|
||
passportDate?: string;
|
||
address?: string;
|
||
position: string;
|
||
department?: string;
|
||
hireDate: string;
|
||
salary?: number;
|
||
phone: string;
|
||
email?: string;
|
||
telegram?: string;
|
||
whatsapp?: string;
|
||
emergencyContact?: string;
|
||
emergencyPhone?: string;
|
||
}
|
||
|
||
interface UpdateEmployeeInput {
|
||
firstName?: string;
|
||
lastName?: string;
|
||
middleName?: string;
|
||
birthDate?: string;
|
||
avatar?: string;
|
||
passportPhoto?: string;
|
||
passportSeries?: string;
|
||
passportNumber?: string;
|
||
passportIssued?: string;
|
||
passportDate?: string;
|
||
address?: string;
|
||
position?: string;
|
||
department?: string;
|
||
hireDate?: string;
|
||
salary?: number;
|
||
status?: "ACTIVE" | "VACATION" | "SICK" | "FIRED";
|
||
phone?: string;
|
||
email?: string;
|
||
telegram?: string;
|
||
whatsapp?: string;
|
||
emergencyContact?: string;
|
||
emergencyPhone?: string;
|
||
}
|
||
|
||
interface UpdateScheduleInput {
|
||
employeeId: string;
|
||
date: string;
|
||
status: "WORK" | "WEEKEND" | "VACATION" | "SICK" | "ABSENT";
|
||
hoursWorked?: number;
|
||
overtimeHours?: number;
|
||
notes?: string;
|
||
}
|
||
|
||
interface AuthTokenPayload {
|
||
userId: string;
|
||
phone: string;
|
||
}
|
||
|
||
// JWT утилиты
|
||
const generateToken = (payload: AuthTokenPayload): string => {
|
||
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: "30d" });
|
||
};
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
const verifyToken = (token: string): AuthTokenPayload => {
|
||
try {
|
||
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
} catch (error) {
|
||
throw new GraphQLError("Недействительный токен", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
};
|
||
|
||
// Скалярный тип для JSON
|
||
const JSONScalar = new GraphQLScalarType({
|
||
name: "JSON",
|
||
description: "JSON custom scalar type",
|
||
serialize(value: unknown) {
|
||
return value; // значение отправляется клиенту
|
||
},
|
||
parseValue(value: unknown) {
|
||
return value; // значение получено от клиента
|
||
},
|
||
parseLiteral(ast) {
|
||
switch (ast.kind) {
|
||
case Kind.STRING:
|
||
case Kind.BOOLEAN:
|
||
return ast.value;
|
||
case Kind.INT:
|
||
case Kind.FLOAT:
|
||
return parseFloat(ast.value);
|
||
case Kind.OBJECT: {
|
||
const value = Object.create(null);
|
||
ast.fields.forEach((field) => {
|
||
value[field.name.value] = parseLiteral(field.value);
|
||
});
|
||
return value;
|
||
}
|
||
case Kind.LIST:
|
||
return ast.values.map(parseLiteral);
|
||
default:
|
||
return null;
|
||
}
|
||
},
|
||
});
|
||
|
||
// Скалярный тип для DateTime
|
||
const DateTimeScalar = new GraphQLScalarType({
|
||
name: "DateTime",
|
||
description: "DateTime custom scalar type",
|
||
serialize(value: unknown) {
|
||
if (value instanceof Date) {
|
||
return value.toISOString(); // значение отправляется клиенту как ISO строка
|
||
}
|
||
return value;
|
||
},
|
||
parseValue(value: unknown) {
|
||
if (typeof value === "string") {
|
||
return new Date(value); // значение получено от клиента, парсим как дату
|
||
}
|
||
return value;
|
||
},
|
||
parseLiteral(ast) {
|
||
if (ast.kind === Kind.STRING) {
|
||
return new Date(ast.value); // AST значение как дата
|
||
}
|
||
return null;
|
||
},
|
||
});
|
||
|
||
function parseLiteral(ast: unknown): unknown {
|
||
const astNode = ast as {
|
||
kind: string;
|
||
value?: unknown;
|
||
fields?: unknown[];
|
||
values?: unknown[];
|
||
};
|
||
|
||
switch (astNode.kind) {
|
||
case Kind.STRING:
|
||
case Kind.BOOLEAN:
|
||
return astNode.value;
|
||
case Kind.INT:
|
||
case Kind.FLOAT:
|
||
return parseFloat(astNode.value as string);
|
||
case Kind.OBJECT: {
|
||
const value = Object.create(null);
|
||
if (astNode.fields) {
|
||
astNode.fields.forEach((field: unknown) => {
|
||
const fieldNode = field as {
|
||
name: { value: string };
|
||
value: unknown;
|
||
};
|
||
value[fieldNode.name.value] = parseLiteral(fieldNode.value);
|
||
});
|
||
}
|
||
return value;
|
||
}
|
||
case Kind.LIST:
|
||
return (ast as { values: unknown[] }).values.map(parseLiteral);
|
||
default:
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export const resolvers = {
|
||
JSON: JSONScalar,
|
||
DateTime: DateTimeScalar,
|
||
|
||
Query: {
|
||
me: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
return await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
},
|
||
|
||
organization: async (
|
||
_: unknown,
|
||
args: { id: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const organization = await prisma.organization.findUnique({
|
||
where: { id: args.id },
|
||
include: {
|
||
apiKeys: true,
|
||
users: true,
|
||
},
|
||
});
|
||
|
||
if (!organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
// Проверяем, что пользователь имеет доступ к этой организации
|
||
const hasAccess = organization.users.some(
|
||
(user) => user.id === context.user!.id
|
||
);
|
||
if (!hasAccess) {
|
||
throw new GraphQLError("Нет доступа к этой организации", {
|
||
extensions: { code: "FORBIDDEN" },
|
||
});
|
||
}
|
||
|
||
return organization;
|
||
},
|
||
|
||
// Поиск организаций по типу для добавления в контрагенты
|
||
searchOrganizations: async (
|
||
_: unknown,
|
||
args: { type?: string; search?: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
// Получаем текущую организацию пользователя
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// Получаем уже существующих контрагентов для добавления флага
|
||
const existingCounterparties = await prisma.counterparty.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
select: { counterpartyId: true },
|
||
});
|
||
|
||
const existingCounterpartyIds = existingCounterparties.map(
|
||
(c) => c.counterpartyId
|
||
);
|
||
|
||
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
|
||
const outgoingRequests = await prisma.counterpartyRequest.findMany({
|
||
where: {
|
||
senderId: currentUser.organization.id,
|
||
status: "PENDING",
|
||
},
|
||
select: { receiverId: true },
|
||
});
|
||
|
||
const outgoingRequestIds = outgoingRequests.map((r) => r.receiverId);
|
||
|
||
// Получаем входящие заявки для добавления флага hasIncomingRequest
|
||
const incomingRequests = await prisma.counterpartyRequest.findMany({
|
||
where: {
|
||
receiverId: currentUser.organization.id,
|
||
status: "PENDING",
|
||
},
|
||
select: { senderId: true },
|
||
});
|
||
|
||
const incomingRequestIds = incomingRequests.map((r) => r.senderId);
|
||
|
||
const where: Record<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 allSupplies = await prisma.supply.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: "desc" },
|
||
});
|
||
|
||
// Получаем все заказы фулфилмента для себя (чтобы исключить их расходники)
|
||
const fulfillmentOwnOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id, // Созданы фулфилментом
|
||
fulfillmentCenterId: currentUser.organization.id, // Для себя
|
||
status: "DELIVERED",
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
// Создаем набор названий товаров из заказов фулфилмента для себя
|
||
const fulfillmentProductNames = new Set(
|
||
fulfillmentOwnOrders.flatMap((order) =>
|
||
order.items.map((item) => item.product.name)
|
||
)
|
||
);
|
||
|
||
// Фильтруем расходники: исключаем те, что созданы заказами фулфилмента для себя
|
||
const sellerSupplies = allSupplies.filter((supply) => {
|
||
// Если расходник соответствует товару из заказа фулфилмента для себя,
|
||
// то это расходник фулфилмента, а не селлера
|
||
return !fulfillmentProductNames.has(supply.name);
|
||
});
|
||
|
||
// Логирование для отладки
|
||
console.log("🔥🔥🔥 SELLER SUPPLIES RESOLVER CALLED 🔥🔥🔥");
|
||
console.log("📊 Расходники селлеров:", {
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
allSuppliesCount: allSupplies.length,
|
||
fulfillmentOwnOrdersCount: fulfillmentOwnOrders.length,
|
||
fulfillmentProductNames: Array.from(fulfillmentProductNames),
|
||
filteredSellerSuppliesCount: sellerSupplies.length,
|
||
sellerOrdersCount: sellerSupplyOrders.length,
|
||
});
|
||
|
||
// Возвращаем только расходники селлеров (исключая расходники фулфилмента)
|
||
return sellerSupplies;
|
||
},
|
||
|
||
// Расходники фулфилмента (материалы для работы фулфилмента)
|
||
myFulfillmentSupplies: async (
|
||
_: unknown,
|
||
__: unknown,
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// TypeScript assertion - мы знаем что organization не null после проверки выше
|
||
const organization = currentUser.organization;
|
||
|
||
// Получаем заказы поставок, созданные этим фулфилмент-центром для себя
|
||
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
organizationId: organization.id, // Создали мы
|
||
fulfillmentCenterId: organization.id, // Получатель - мы
|
||
status: {
|
||
in: ["PENDING", "CONFIRMED", "IN_TRANSIT", "DELIVERED"], // Все статусы
|
||
},
|
||
},
|
||
include: {
|
||
partner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: "desc" },
|
||
});
|
||
|
||
// Преобразуем заказы поставок в формат supply для единообразия
|
||
const fulfillmentSupplies = fulfillmentSupplyOrders.flatMap((order) =>
|
||
order.items.map((item) => ({
|
||
id: `fulfillment-order-${order.id}-${item.id}`,
|
||
name: item.product.name,
|
||
description:
|
||
item.product.description || `Расходники от ${order.partner.name}`,
|
||
price: item.price,
|
||
quantity: item.quantity,
|
||
unit: "шт",
|
||
category: item.product.category?.name || "Расходники фулфилмента",
|
||
status:
|
||
order.status === "PENDING"
|
||
? "planned"
|
||
: order.status === "CONFIRMED"
|
||
? "confirmed"
|
||
: order.status === "IN_TRANSIT"
|
||
? "in-transit"
|
||
: order.status === "DELIVERED"
|
||
? "in-stock"
|
||
: "planned",
|
||
date: order.createdAt,
|
||
supplier: order.partner.name || order.partner.fullName || "Не указан",
|
||
minStock: Math.round(item.quantity * 0.1),
|
||
currentStock: order.status === "DELIVERED" ? item.quantity : 0,
|
||
usedStock: 0, // TODO: Подсчитывать реальное использование
|
||
imageUrl: null,
|
||
createdAt: order.createdAt,
|
||
updatedAt: order.updatedAt,
|
||
organizationId: organization.id,
|
||
organization: organization,
|
||
shippedQuantity: 0,
|
||
}))
|
||
);
|
||
|
||
// Логирование для отладки
|
||
console.log("🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED 🔥🔥🔥");
|
||
console.log("📊 Расходники фулфилмента:", {
|
||
organizationId: organization.id,
|
||
organizationType: organization.type,
|
||
fulfillmentOrdersCount: fulfillmentSupplyOrders.length,
|
||
fulfillmentSuppliesCount: fulfillmentSupplies.length,
|
||
fulfillmentOrders: fulfillmentSupplyOrders.map((o) => ({
|
||
id: o.id,
|
||
supplierName: o.partner.name,
|
||
status: o.status,
|
||
itemsCount: o.items.length,
|
||
})),
|
||
});
|
||
|
||
return fulfillmentSupplies;
|
||
},
|
||
|
||
// Заказы поставок расходников
|
||
supplyOrders: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// Возвращаем заказы где текущая организация является заказчиком, поставщиком, получателем или логистическим партнером
|
||
return await prisma.supplyOrder.findMany({
|
||
where: {
|
||
OR: [
|
||
{ organizationId: currentUser.organization.id }, // Заказы созданные организацией
|
||
{ partnerId: currentUser.organization.id }, // Заказы где организация - поставщик
|
||
{ fulfillmentCenterId: currentUser.organization.id }, // Заказы где организация - получатель (фулфилмент)
|
||
{ logisticsPartnerId: currentUser.organization.id }, // Заказы где организация - логистический партнер
|
||
],
|
||
},
|
||
include: {
|
||
partner: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
fulfillmentCenter: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: "desc" },
|
||
});
|
||
},
|
||
|
||
// Счетчик поставок, требующих одобрения
|
||
pendingSuppliesCount: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// Считаем заказы поставок, требующие действий
|
||
|
||
// Расходники фулфилмента (созданные нами для себя) - требуют действий по статусам
|
||
const ourSupplyOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
organizationId: currentUser.organization.id, // Создали мы
|
||
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
|
||
status: { in: ["CONFIRMED", "IN_TRANSIT"] }, // Подтверждено или в пути
|
||
},
|
||
});
|
||
|
||
// Расходники селлеров (созданные другими для нас) - требуют подтверждения получения
|
||
const sellerSupplyOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
|
||
organizationId: { not: currentUser.organization.id }, // Создали НЕ мы
|
||
status: "IN_TRANSIT", // В пути - нужно подтвердить получение
|
||
},
|
||
});
|
||
|
||
// 🔔 ВХОДЯЩИЕ ЗАКАЗЫ ДЛЯ ПОСТАВЩИКОВ (WHOLESALE) - требуют подтверждения
|
||
const incomingSupplierOrders = await prisma.supplyOrder.count({
|
||
where: {
|
||
partnerId: currentUser.organization.id, // Мы - поставщик
|
||
status: "PENDING", // Ожидает подтверждения от поставщика
|
||
},
|
||
});
|
||
|
||
// Общий счетчик поставок
|
||
const pendingSupplyOrders =
|
||
ourSupplyOrders + sellerSupplyOrders + incomingSupplierOrders;
|
||
|
||
// Считаем входящие заявки на партнерство со статусом PENDING
|
||
const pendingIncomingRequests = await prisma.counterpartyRequest.count({
|
||
where: {
|
||
receiverId: currentUser.organization.id,
|
||
status: "PENDING",
|
||
},
|
||
});
|
||
|
||
return {
|
||
supplyOrders: pendingSupplyOrders,
|
||
ourSupplyOrders: ourSupplyOrders, // Расходники фулфилмента
|
||
sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров
|
||
incomingSupplierOrders: incomingSupplierOrders, // 🔔 Входящие заказы для поставщиков
|
||
incomingRequests: pendingIncomingRequests,
|
||
total: pendingSupplyOrders + pendingIncomingRequests,
|
||
};
|
||
},
|
||
|
||
// Статистика склада фулфилмента с изменениями за сутки
|
||
fulfillmentWarehouseStats: async (_: unknown, __: unknown, context: Context) => {
|
||
console.log("🔥 FULFILLMENT WAREHOUSE STATS RESOLVER CALLED");
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
if (currentUser.organization.type !== "FULFILLMENT") {
|
||
throw new GraphQLError("Доступ разрешен только для фулфилмент-центров");
|
||
}
|
||
|
||
const organizationId = currentUser.organization.id;
|
||
|
||
// Получаем дату начала суток (24 часа назад)
|
||
const oneDayAgo = new Date();
|
||
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
|
||
|
||
console.log(`🏢 Organization ID: ${organizationId}, Date 24h ago: ${oneDayAgo.toISOString()}`);
|
||
|
||
// Сначала проверим ВСЕ заказы поставок
|
||
const allSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: { status: "DELIVERED" },
|
||
include: {
|
||
items: {
|
||
include: { product: true }
|
||
},
|
||
organization: { select: { id: true, name: true, type: true } }
|
||
}
|
||
});
|
||
console.log(`📦 ALL DELIVERED ORDERS: ${allSupplyOrders.length}`);
|
||
allSupplyOrders.forEach(order => {
|
||
console.log(` Order ${order.id}: org=${order.organizationId} (${order.organization?.name}), fulfillment=${order.fulfillmentCenterId}, items=${order.items.length}`);
|
||
});
|
||
|
||
// Продукты (товары от селлеров) - заказы К нам, но исключаем расходники фулфилмента
|
||
const allDeliveredOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
fulfillmentCenterId: organizationId, // Доставлено к нам (фулфилменту)
|
||
status: "DELIVERED"
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true }
|
||
}
|
||
}
|
||
});
|
||
console.log(`🛒 ALL ORDERS TO FULFILLMENT: ${allDeliveredOrders.length}`);
|
||
|
||
const productsCount = sellerDeliveredOrders.reduce((sum, order) =>
|
||
sum + order.items.reduce((itemSum, item) =>
|
||
itemSum + (item.product.type === "PRODUCT" ? item.quantity : 0), 0
|
||
), 0
|
||
);
|
||
// Изменения товаров за сутки (от селлеров)
|
||
const recentSellerDeliveredOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
fulfillmentCenterId: organizationId, // К нам
|
||
organizationId: { not: organizationId }, // От селлеров
|
||
status: "DELIVERED",
|
||
updatedAt: { gte: oneDayAgo }
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true }
|
||
}
|
||
}
|
||
});
|
||
|
||
const productsChangeToday = recentSellerDeliveredOrders.reduce((sum, order) =>
|
||
sum + order.items.reduce((itemSum, item) =>
|
||
itemSum + (item.product.type === "PRODUCT" ? item.quantity : 0), 0
|
||
), 0
|
||
);
|
||
|
||
// Товары (готовые товары = все продукты, не расходники)
|
||
const goodsCount = productsCount; // Готовые товары = все продукты
|
||
const goodsChangeToday = productsChangeToday; // Изменения товаров = изменения продуктов
|
||
|
||
// Брак
|
||
const defectsCount = 0; // TODO: реальные данные о браке
|
||
const defectsChangeToday = 0;
|
||
|
||
// Возвраты с ПВЗ
|
||
const pvzReturnsCount = 0; // TODO: реальные данные о возвратах
|
||
const pvzReturnsChangeToday = 0;
|
||
|
||
// Расходники фулфилмента - заказы ОТ фулфилмента К поставщикам
|
||
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Заказчик = фулфилмент
|
||
fulfillmentCenterId: null, // Не является доставкой к фулфилменту
|
||
status: "DELIVERED"
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true }
|
||
}
|
||
}
|
||
});
|
||
console.log(`🏭 FULFILLMENT SUPPLY ORDERS: ${fulfillmentSupplyOrders.length}`);
|
||
const fulfillmentSuppliesCount = fulfillmentSupplyOrders.reduce(
|
||
(sum, order) => sum + order.items.reduce((itemSum, item) =>
|
||
itemSum + (item.product.type === "CONSUMABLE" ? item.quantity : 0), 0
|
||
), 0
|
||
);
|
||
|
||
console.log(`🔥 FULFILLMENT SUPPLIES DEBUG: organizationId=${organizationId}, totalOrders=${fulfillmentSupplyOrders.length}, totalCount=${fulfillmentSuppliesCount}`);
|
||
|
||
// Изменения расходников фулфилмента за сутки
|
||
const fulfillmentSuppliesReceivedToday = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
organizationId: organizationId, // Заказчик = фулфилмент
|
||
fulfillmentCenterId: null, // Не доставка к фулфилменту
|
||
status: "DELIVERED",
|
||
updatedAt: { gte: oneDayAgo }
|
||
},
|
||
include: {
|
||
items: {
|
||
include: { product: true }
|
||
}
|
||
}
|
||
});
|
||
const fulfillmentSuppliesChangeToday = fulfillmentSuppliesReceivedToday.reduce(
|
||
(sum, order) => sum + order.items.reduce((itemSum, item) =>
|
||
itemSum + (item.product.type === "CONSUMABLE" ? item.quantity : 0), 0
|
||
), 0
|
||
);
|
||
|
||
console.log(`📊 FULFILLMENT SUPPLIES RECEIVED TODAY: ${fulfillmentSuppliesReceivedToday.length} orders, ${fulfillmentSuppliesChangeToday} items`);
|
||
|
||
// Расходники селлеров - получаем из заказов от селлеров (расходники = CONSUMABLE)
|
||
const sellerSuppliesCount = sellerDeliveredOrders.reduce((sum, order) =>
|
||
sum + order.items.reduce((itemSum, item) =>
|
||
itemSum + (item.product.type === "CONSUMABLE" ? item.quantity : 0), 0
|
||
), 0
|
||
);
|
||
|
||
console.log(`💼 SELLER SUPPLIES DEBUG: totalCount=${sellerSuppliesCount} (from delivered orders)`);
|
||
|
||
// Изменения расходников селлеров за сутки - используем уже полученные данные
|
||
const sellerSuppliesChangeToday = recentSellerDeliveredOrders.reduce((sum, order) =>
|
||
sum + order.items.reduce((itemSum, item) =>
|
||
itemSum + (item.product.type === "CONSUMABLE" ? item.quantity : 0), 0
|
||
), 0
|
||
);
|
||
|
||
console.log(`📊 SELLER SUPPLIES RECEIVED TODAY: ${recentSellerDeliveredOrders.length} orders, ${sellerSuppliesChangeToday} items`);
|
||
|
||
// Вычисляем процентные изменения
|
||
const calculatePercentChange = (current: number, change: number): number => {
|
||
if (current === 0) return change > 0 ? 100 : 0;
|
||
return (change / current) * 100;
|
||
};
|
||
|
||
const result = {
|
||
products: {
|
||
current: productsCount,
|
||
change: productsChangeToday,
|
||
percentChange: calculatePercentChange(productsCount, productsChangeToday)
|
||
},
|
||
goods: {
|
||
current: goodsCount,
|
||
change: goodsChangeToday,
|
||
percentChange: calculatePercentChange(goodsCount, goodsChangeToday)
|
||
},
|
||
defects: {
|
||
current: defectsCount,
|
||
change: defectsChangeToday,
|
||
percentChange: calculatePercentChange(defectsCount, defectsChangeToday)
|
||
},
|
||
pvzReturns: {
|
||
current: pvzReturnsCount,
|
||
change: pvzReturnsChangeToday,
|
||
percentChange: calculatePercentChange(pvzReturnsCount, pvzReturnsChangeToday)
|
||
},
|
||
fulfillmentSupplies: {
|
||
current: fulfillmentSuppliesCount,
|
||
change: fulfillmentSuppliesChangeToday,
|
||
percentChange: calculatePercentChange(fulfillmentSuppliesCount, fulfillmentSuppliesChangeToday)
|
||
},
|
||
sellerSupplies: {
|
||
current: sellerSuppliesCount,
|
||
change: sellerSuppliesChangeToday,
|
||
percentChange: calculatePercentChange(sellerSuppliesCount, sellerSuppliesChangeToday)
|
||
}
|
||
};
|
||
|
||
console.log(`🏁 FINAL WAREHOUSE STATS RESULT:`, JSON.stringify(result, null, 2));
|
||
|
||
return result;
|
||
},
|
||
|
||
// Логистика организации
|
||
myLogistics: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
return await prisma.logistics.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: "desc" },
|
||
});
|
||
},
|
||
|
||
// Мои поставки Wildberries
|
||
myWildberriesSupplies: async (
|
||
_: unknown,
|
||
__: unknown,
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
return await prisma.wildberriesSupply.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
organization: true,
|
||
cards: true,
|
||
},
|
||
orderBy: { createdAt: "desc" },
|
||
});
|
||
},
|
||
|
||
// Мои товары и расходники (для поставщиков)
|
||
myProducts: async (_: unknown, __: unknown, context: Context) => {
|
||
console.log("🔍 MY_PRODUCTS RESOLVER - ВЫЗВАН:", {
|
||
hasUser: !!context.user,
|
||
userId: context.user?.id,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
console.log("👤 ПОЛЬЗОВАТЕЛЬ НАЙДЕН:", {
|
||
userId: currentUser?.id,
|
||
hasOrganization: !!currentUser?.organization,
|
||
organizationType: currentUser?.organization?.type,
|
||
organizationName: currentUser?.organization?.name,
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// Проверяем, что это поставщик
|
||
if (currentUser.organization.type !== "WHOLESALE") {
|
||
console.log("❌ ДОСТУП ЗАПРЕЩЕН - НЕ ПОСТАВЩИК:", {
|
||
actualType: currentUser.organization.type,
|
||
requiredType: "WHOLESALE",
|
||
});
|
||
throw new GraphQLError("Товары доступны только для поставщиков");
|
||
}
|
||
|
||
const products = await prisma.product.findMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
// Показываем и товары, и расходники поставщика
|
||
},
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
orderBy: { createdAt: "desc" },
|
||
});
|
||
|
||
console.log("🔥 MY_PRODUCTS RESOLVER DEBUG:", {
|
||
userId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
organizationName: currentUser.organization.name,
|
||
totalProducts: products.length,
|
||
productTypes: products.map((p) => ({
|
||
id: p.id,
|
||
name: p.name,
|
||
article: p.article,
|
||
type: p.type,
|
||
isActive: p.isActive,
|
||
createdAt: p.createdAt,
|
||
})),
|
||
});
|
||
|
||
return products;
|
||
},
|
||
|
||
// Товары на складе фулфилмента (из доставленных заказов поставок)
|
||
warehouseProducts: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// Проверяем, что это фулфилмент центр
|
||
if (currentUser.organization.type !== "FULFILLMENT") {
|
||
throw new GraphQLError(
|
||
"Товары склада доступны только для фулфилмент центров"
|
||
);
|
||
}
|
||
|
||
// Получаем все доставленные заказы поставок, где этот фулфилмент центр является получателем
|
||
const deliveredSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
fulfillmentCenterId: currentUser.organization.id,
|
||
status: "DELIVERED", // Только доставленные заказы
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true, // Включаем информацию о поставщике
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true, // Селлер, который сделал заказ
|
||
partner: true, // Поставщик товаров
|
||
},
|
||
});
|
||
|
||
// Собираем все товары из доставленных заказов
|
||
const allProducts: unknown[] = [];
|
||
|
||
console.log("🔍 Резолвер warehouseProducts (доставленные заказы):", {
|
||
currentUserId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
deliveredOrdersCount: deliveredSupplyOrders.length,
|
||
orders: deliveredSupplyOrders.map((order) => ({
|
||
id: order.id,
|
||
sellerName: order.organization.name || order.organization.fullName,
|
||
supplierName: order.partner.name || order.partner.fullName,
|
||
status: order.status,
|
||
itemsCount: order.items.length,
|
||
deliveryDate: order.deliveryDate,
|
||
})),
|
||
});
|
||
|
||
for (const order of deliveredSupplyOrders) {
|
||
console.log(
|
||
`📦 Заказ от селлера ${order.organization.name} у поставщика ${order.partner.name}:`,
|
||
order.items.map((item) => ({
|
||
productId: item.product.id,
|
||
productName: item.product.name,
|
||
article: item.product.article,
|
||
orderedQuantity: item.quantity,
|
||
price: item.price,
|
||
}))
|
||
);
|
||
|
||
for (const item of order.items) {
|
||
// Добавляем только товары типа PRODUCT, исключаем расходники
|
||
if (item.product.type === "PRODUCT") {
|
||
allProducts.push({
|
||
...item.product,
|
||
// Дополнительная информация о заказе
|
||
orderedQuantity: item.quantity,
|
||
orderedPrice: item.price,
|
||
orderId: order.id,
|
||
orderDate: order.deliveryDate,
|
||
seller: order.organization, // Селлер, который заказал
|
||
supplier: order.partner, // Поставщик товара
|
||
// Для совместимости с существующим интерфейсом
|
||
organization: order.organization, // Указываем селлера как владельца
|
||
});
|
||
} else {
|
||
console.log(
|
||
`🚫 Исключен расходник из основного склада фулфилмента:`,
|
||
{
|
||
name: item.product.name,
|
||
type: item.product.type,
|
||
orderId: order.id,
|
||
}
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(
|
||
"✅ Итого товаров на складе фулфилмента (из доставленных заказов):",
|
||
allProducts.length
|
||
);
|
||
return allProducts;
|
||
},
|
||
|
||
// Все товары и расходники поставщиков для маркета
|
||
allProducts: async (
|
||
_: unknown,
|
||
args: { search?: string; category?: string },
|
||
context: Context
|
||
) => {
|
||
console.log("🛍️ ALL_PRODUCTS RESOLVER - ВЫЗВАН:", {
|
||
userId: context.user?.id,
|
||
search: args.search,
|
||
category: args.category,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const where: Record<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,
|
||
logisticsPartnerId: args.input.logisticsPartnerId,
|
||
status: initialStatus,
|
||
items: {
|
||
create: orderItems,
|
||
},
|
||
},
|
||
include: {
|
||
partner: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
fulfillmentCenter: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
logisticsPartner: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
// Создаем расходники на основе заказанных товаров
|
||
// Расходники создаются в организации получателя (фулфилмент-центре)
|
||
const suppliesData = args.input.items.map((item) => {
|
||
const product = products.find((p) => p.id === item.productId)!;
|
||
const productWithCategory = supplyOrder.items.find(
|
||
(orderItem: {
|
||
productId: string;
|
||
product: { category?: { name: string } | null };
|
||
}) => orderItem.productId === item.productId
|
||
)?.product;
|
||
|
||
return {
|
||
name: product.name,
|
||
description: product.description || `Заказано у ${partner.name}`,
|
||
price: product.price,
|
||
quantity: item.quantity,
|
||
unit: "шт",
|
||
category: productWithCategory?.category?.name || "Расходники",
|
||
status: "planned", // Статус "запланировано" (ожидает одобрения поставщиком)
|
||
date: new Date(args.input.deliveryDate),
|
||
supplier: partner.name || partner.fullName || "Не указан",
|
||
minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток
|
||
currentStock: 0, // Пока товар не пришел
|
||
// Расходники создаются в организации получателя (фулфилмент-центре)
|
||
organizationId: fulfillmentCenterId || currentUser.organization!.id,
|
||
};
|
||
});
|
||
|
||
// Создаем расходники
|
||
await prisma.supply.createMany({
|
||
data: suppliesData,
|
||
});
|
||
|
||
// 🔔 ОТПРАВЛЯЕМ УВЕДОМЛЕНИЕ ПОСТАВЩИКУ О НОВОМ ЗАКАЗЕ
|
||
try {
|
||
const orderSummary = args.input.items
|
||
.map((item) => {
|
||
const product = products.find((p) => p.id === item.productId)!;
|
||
return `${product.name} - ${item.quantity} шт.`;
|
||
})
|
||
.join(", ");
|
||
|
||
const notificationMessage = `🔔 Новый заказ поставки от ${
|
||
currentUser.organization.name || currentUser.organization.fullName
|
||
}!\n\nТовары: ${orderSummary}\nДата доставки: ${new Date(
|
||
args.input.deliveryDate
|
||
).toLocaleDateString(
|
||
"ru-RU"
|
||
)}\nОбщая сумма: ${totalAmount.toLocaleString(
|
||
"ru-RU"
|
||
)} ₽\n\nПожалуйста, подтвердите заказ в разделе "Поставки".`;
|
||
|
||
await prisma.message.create({
|
||
data: {
|
||
content: notificationMessage,
|
||
type: "TEXT",
|
||
senderId: context.user.id,
|
||
senderOrganizationId: currentUser.organization.id,
|
||
receiverOrganizationId: args.input.partnerId,
|
||
},
|
||
});
|
||
|
||
console.log(`✅ Уведомление отправлено поставщику ${partner.name}`);
|
||
} catch (notificationError) {
|
||
console.error("❌ Ошибка отправки уведомления:", notificationError);
|
||
// Не прерываем выполнение, если уведомление не отправилось
|
||
}
|
||
|
||
// Формируем сообщение в зависимости от роли организации
|
||
let successMessage = "";
|
||
if (organizationRole === "SELLER") {
|
||
successMessage = `Заказ поставки расходников создан! Расходники будут доставлены ${
|
||
fulfillmentCenterId
|
||
? "на указанный фулфилмент-склад"
|
||
: "согласно настройкам"
|
||
}. Ожидайте подтверждения от поставщика.`;
|
||
} else if (organizationRole === "FULFILLMENT") {
|
||
successMessage = `Заказ поставки расходников создан для вашего склада! Ожидайте подтверждения от поставщика и координации с логистикой.`;
|
||
} else if (organizationRole === "LOGIST") {
|
||
successMessage = `Заказ поставки создан и подтвержден! Координируйте доставку расходников от поставщика на фулфилмент-склад.`;
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: successMessage,
|
||
order: supplyOrder,
|
||
processInfo: {
|
||
role: organizationRole,
|
||
supplier: partner.name || partner.fullName,
|
||
fulfillmentCenter: fulfillmentCenterId,
|
||
logistics: args.input.logisticsPartnerId,
|
||
status: initialStatus,
|
||
},
|
||
};
|
||
} catch (error) {
|
||
console.error("Error creating supply order:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при создании заказа поставки",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Создать товар
|
||
createProduct: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
name: string;
|
||
article: string;
|
||
description?: string;
|
||
price: number;
|
||
pricePerSet?: number;
|
||
quantity: number;
|
||
setQuantity?: number;
|
||
ordered?: number;
|
||
inTransit?: number;
|
||
stock?: number;
|
||
sold?: number;
|
||
type?: "PRODUCT" | "CONSUMABLE";
|
||
categoryId?: string;
|
||
brand?: string;
|
||
color?: string;
|
||
size?: string;
|
||
weight?: number;
|
||
dimensions?: string;
|
||
material?: string;
|
||
images?: string[];
|
||
mainImage?: string;
|
||
isActive?: boolean;
|
||
};
|
||
},
|
||
context: Context
|
||
) => {
|
||
console.log("🆕 CREATE_PRODUCT RESOLVER - ВЫЗВАН:", {
|
||
hasUser: !!context.user,
|
||
userId: context.user?.id,
|
||
inputData: args.input,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// Проверяем, что это поставщик
|
||
if (currentUser.organization.type !== "WHOLESALE") {
|
||
throw new GraphQLError("Товары доступны только для поставщиков");
|
||
}
|
||
|
||
// Проверяем уникальность артикула в рамках организации
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
article: args.input.article,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
});
|
||
|
||
if (existingProduct) {
|
||
return {
|
||
success: false,
|
||
message: "Товар с таким артикулом уже существует",
|
||
};
|
||
}
|
||
|
||
try {
|
||
console.log("🛍️ СОЗДАНИЕ ТОВАРА - НАЧАЛО:", {
|
||
userId: currentUser.id,
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
productData: {
|
||
name: args.input.name,
|
||
article: args.input.article,
|
||
type: args.input.type || "PRODUCT",
|
||
isActive: args.input.isActive ?? true,
|
||
},
|
||
});
|
||
|
||
const product = await prisma.product.create({
|
||
data: {
|
||
name: args.input.name,
|
||
article: args.input.article,
|
||
description: args.input.description,
|
||
price: args.input.price,
|
||
pricePerSet: args.input.pricePerSet,
|
||
quantity: args.input.quantity,
|
||
setQuantity: args.input.setQuantity,
|
||
ordered: args.input.ordered,
|
||
inTransit: args.input.inTransit,
|
||
stock: args.input.stock,
|
||
sold: args.input.sold,
|
||
type: args.input.type || "PRODUCT",
|
||
categoryId: args.input.categoryId,
|
||
brand: args.input.brand,
|
||
color: args.input.color,
|
||
size: args.input.size,
|
||
weight: args.input.weight,
|
||
dimensions: args.input.dimensions,
|
||
material: args.input.material,
|
||
images: JSON.stringify(args.input.images || []),
|
||
mainImage: args.input.mainImage,
|
||
isActive: args.input.isActive ?? true,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
console.log("✅ ТОВАР УСПЕШНО СОЗДАН:", {
|
||
productId: product.id,
|
||
name: product.name,
|
||
article: product.article,
|
||
type: product.type,
|
||
isActive: product.isActive,
|
||
organizationId: product.organizationId,
|
||
createdAt: product.createdAt,
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Товар успешно создан",
|
||
product,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error creating product:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при создании товара",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Обновить товар
|
||
updateProduct: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string;
|
||
input: {
|
||
name: string;
|
||
article: string;
|
||
description?: string;
|
||
price: number;
|
||
pricePerSet?: number;
|
||
quantity: number;
|
||
setQuantity?: number;
|
||
ordered?: number;
|
||
inTransit?: number;
|
||
stock?: number;
|
||
sold?: number;
|
||
type?: "PRODUCT" | "CONSUMABLE";
|
||
categoryId?: string;
|
||
brand?: string;
|
||
color?: string;
|
||
size?: string;
|
||
weight?: number;
|
||
dimensions?: string;
|
||
material?: string;
|
||
images?: string[];
|
||
mainImage?: string;
|
||
isActive?: boolean;
|
||
};
|
||
},
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// Проверяем, что товар принадлежит текущей организации
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
});
|
||
|
||
if (!existingProduct) {
|
||
throw new GraphQLError("Товар не найден или нет доступа");
|
||
}
|
||
|
||
// Проверяем уникальность артикула (если он изменился)
|
||
if (args.input.article !== existingProduct.article) {
|
||
const duplicateProduct = await prisma.product.findFirst({
|
||
where: {
|
||
article: args.input.article,
|
||
organizationId: currentUser.organization.id,
|
||
NOT: { id: args.id },
|
||
},
|
||
});
|
||
|
||
if (duplicateProduct) {
|
||
return {
|
||
success: false,
|
||
message: "Товар с таким артикулом уже существует",
|
||
};
|
||
}
|
||
}
|
||
|
||
try {
|
||
const product = await prisma.product.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
article: args.input.article,
|
||
description: args.input.description,
|
||
price: args.input.price,
|
||
pricePerSet: args.input.pricePerSet,
|
||
quantity: args.input.quantity,
|
||
setQuantity: args.input.setQuantity,
|
||
ordered: args.input.ordered,
|
||
inTransit: args.input.inTransit,
|
||
stock: args.input.stock,
|
||
sold: args.input.sold,
|
||
...(args.input.type && { type: args.input.type }),
|
||
categoryId: args.input.categoryId,
|
||
brand: args.input.brand,
|
||
color: args.input.color,
|
||
size: args.input.size,
|
||
weight: args.input.weight,
|
||
dimensions: args.input.dimensions,
|
||
material: args.input.material,
|
||
images: args.input.images
|
||
? JSON.stringify(args.input.images)
|
||
: undefined,
|
||
mainImage: args.input.mainImage,
|
||
isActive: args.input.isActive ?? true,
|
||
},
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Товар успешно обновлен",
|
||
product,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error updating product:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при обновлении товара",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Удалить товар
|
||
deleteProduct: async (
|
||
_: unknown,
|
||
args: { id: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// Проверяем, что товар принадлежит текущей организации
|
||
const existingProduct = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
});
|
||
|
||
if (!existingProduct) {
|
||
throw new GraphQLError("Товар не найден или нет доступа");
|
||
}
|
||
|
||
try {
|
||
await prisma.product.delete({
|
||
where: { id: args.id },
|
||
});
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Error deleting product:", error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// Создать категорию
|
||
createCategory: async (
|
||
_: unknown,
|
||
args: { input: { name: string } },
|
||
context: Context
|
||
) => {
|
||
if (!context.user && !context.admin) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
// Проверяем уникальность названия категории
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { name: args.input.name },
|
||
});
|
||
|
||
if (existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: "Категория с таким названием уже существует",
|
||
};
|
||
}
|
||
|
||
try {
|
||
const category = await prisma.category.create({
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Категория успешно создана",
|
||
category,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error creating category:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при создании категории",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Обновить категорию
|
||
updateCategory: async (
|
||
_: unknown,
|
||
args: { id: string; input: { name: string } },
|
||
context: Context
|
||
) => {
|
||
if (!context.user && !context.admin) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
// Проверяем существование категории
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
});
|
||
|
||
if (!existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: "Категория не найдена",
|
||
};
|
||
}
|
||
|
||
// Проверяем уникальность нового названия (если изменилось)
|
||
if (args.input.name !== existingCategory.name) {
|
||
const duplicateCategory = await prisma.category.findUnique({
|
||
where: { name: args.input.name },
|
||
});
|
||
|
||
if (duplicateCategory) {
|
||
return {
|
||
success: false,
|
||
message: "Категория с таким названием уже существует",
|
||
};
|
||
}
|
||
}
|
||
|
||
try {
|
||
const category = await prisma.category.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Категория успешно обновлена",
|
||
category,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error updating category:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при обновлении категории",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Удалить категорию
|
||
deleteCategory: async (
|
||
_: unknown,
|
||
args: { id: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user && !context.admin) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
// Проверяем существование категории
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
include: { products: true },
|
||
});
|
||
|
||
if (!existingCategory) {
|
||
throw new GraphQLError("Категория не найдена");
|
||
}
|
||
|
||
// Проверяем, есть ли товары в этой категории
|
||
if (existingCategory.products.length > 0) {
|
||
throw new GraphQLError(
|
||
"Нельзя удалить категорию, в которой есть товары"
|
||
);
|
||
}
|
||
|
||
try {
|
||
await prisma.category.delete({
|
||
where: { id: args.id },
|
||
});
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Error deleting category:", error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// Добавить товар в корзину
|
||
addToCart: async (
|
||
_: unknown,
|
||
args: { productId: string; quantity: number },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// Проверяем, что товар существует и активен
|
||
const product = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.productId,
|
||
isActive: true,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: "Товар не найден или неактивен",
|
||
};
|
||
}
|
||
|
||
// Проверяем, что пользователь не пытается добавить свой собственный товар
|
||
if (product.organizationId === currentUser.organization.id) {
|
||
return {
|
||
success: false,
|
||
message: "Нельзя добавлять собственные товары в корзину",
|
||
};
|
||
}
|
||
|
||
// Найти или создать корзину
|
||
let cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
});
|
||
|
||
if (!cart) {
|
||
cart = await prisma.cart.create({
|
||
data: {
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
});
|
||
}
|
||
|
||
try {
|
||
// Проверяем, есть ли уже такой товар в корзине
|
||
const existingCartItem = await prisma.cartItem.findUnique({
|
||
where: {
|
||
cartId_productId: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
});
|
||
|
||
if (existingCartItem) {
|
||
// Обновляем количество
|
||
const newQuantity = existingCartItem.quantity + args.quantity;
|
||
|
||
if (newQuantity > product.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
|
||
};
|
||
}
|
||
|
||
await prisma.cartItem.update({
|
||
where: { id: existingCartItem.id },
|
||
data: { quantity: newQuantity },
|
||
});
|
||
} else {
|
||
// Создаем новый элемент корзины
|
||
if (args.quantity > product.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
|
||
};
|
||
}
|
||
|
||
await prisma.cartItem.create({
|
||
data: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
quantity: args.quantity,
|
||
},
|
||
});
|
||
}
|
||
|
||
// Возвращаем обновленную корзину
|
||
const updatedCart = await prisma.cart.findUnique({
|
||
where: { id: cart.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Товар добавлен в корзину",
|
||
cart: updatedCart,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error adding to cart:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при добавлении в корзину",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Обновить количество товара в корзине
|
||
updateCartItem: async (
|
||
_: unknown,
|
||
args: { productId: string; quantity: number },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
const cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
});
|
||
|
||
if (!cart) {
|
||
return {
|
||
success: false,
|
||
message: "Корзина не найдена",
|
||
};
|
||
}
|
||
|
||
// Проверяем, что товар существует в корзине
|
||
const cartItem = await prisma.cartItem.findUnique({
|
||
where: {
|
||
cartId_productId: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
include: {
|
||
product: true,
|
||
},
|
||
});
|
||
|
||
if (!cartItem) {
|
||
return {
|
||
success: false,
|
||
message: "Товар не найден в корзине",
|
||
};
|
||
}
|
||
|
||
if (args.quantity <= 0) {
|
||
return {
|
||
success: false,
|
||
message: "Количество должно быть больше 0",
|
||
};
|
||
}
|
||
|
||
if (args.quantity > cartItem.product.quantity) {
|
||
return {
|
||
success: false,
|
||
message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}`,
|
||
};
|
||
}
|
||
|
||
try {
|
||
await prisma.cartItem.update({
|
||
where: { id: cartItem.id },
|
||
data: { quantity: args.quantity },
|
||
});
|
||
|
||
// Возвращаем обновленную корзину
|
||
const updatedCart = await prisma.cart.findUnique({
|
||
where: { id: cart.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Количество товара обновлено",
|
||
cart: updatedCart,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error updating cart item:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при обновлении корзины",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Удалить товар из корзины
|
||
removeFromCart: async (
|
||
_: unknown,
|
||
args: { productId: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
const cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
});
|
||
|
||
if (!cart) {
|
||
return {
|
||
success: false,
|
||
message: "Корзина не найдена",
|
||
};
|
||
}
|
||
|
||
try {
|
||
await prisma.cartItem.delete({
|
||
where: {
|
||
cartId_productId: {
|
||
cartId: cart.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
});
|
||
|
||
// Возвращаем обновленную корзину
|
||
const updatedCart = await prisma.cart.findUnique({
|
||
where: { id: cart.id },
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Товар удален из корзины",
|
||
cart: updatedCart,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error removing from cart:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при удалении из корзины",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Очистить корзину
|
||
clearCart: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
const cart = await prisma.cart.findUnique({
|
||
where: { organizationId: currentUser.organization.id },
|
||
});
|
||
|
||
if (!cart) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
await prisma.cartItem.deleteMany({
|
||
where: { cartId: cart.id },
|
||
});
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Error clearing cart:", error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// Добавить товар в избранное
|
||
addToFavorites: async (
|
||
_: unknown,
|
||
args: { productId: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
// Проверяем, что товар существует и активен
|
||
const product = await prisma.product.findFirst({
|
||
where: {
|
||
id: args.productId,
|
||
isActive: true,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
if (!product) {
|
||
return {
|
||
success: false,
|
||
message: "Товар не найден или неактивен",
|
||
};
|
||
}
|
||
|
||
// Проверяем, что пользователь не пытается добавить свой собственный товар
|
||
if (product.organizationId === currentUser.organization.id) {
|
||
return {
|
||
success: false,
|
||
message: "Нельзя добавлять собственные товары в избранное",
|
||
};
|
||
}
|
||
|
||
try {
|
||
// Проверяем, есть ли уже такой товар в избранном
|
||
const existingFavorite = await prisma.favorites.findUnique({
|
||
where: {
|
||
organizationId_productId: {
|
||
organizationId: currentUser.organization.id,
|
||
productId: args.productId,
|
||
},
|
||
},
|
||
});
|
||
|
||
if (existingFavorite) {
|
||
return {
|
||
success: false,
|
||
message: "Товар уже в избранном",
|
||
};
|
||
}
|
||
|
||
// Добавляем товар в избранное
|
||
await prisma.favorites.create({
|
||
data: {
|
||
organizationId: currentUser.organization.id,
|
||
productId: args.productId,
|
||
},
|
||
});
|
||
|
||
// Возвращаем обновленный список избранного
|
||
const favorites = await prisma.favorites.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: "desc" },
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Товар добавлен в избранное",
|
||
favorites: favorites.map((favorite) => favorite.product),
|
||
};
|
||
} catch (error) {
|
||
console.error("Error adding to favorites:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при добавлении в избранное",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Удалить товар из избранного
|
||
removeFromFavorites: async (
|
||
_: unknown,
|
||
args: { productId: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
// Удаляем товар из избранного
|
||
await prisma.favorites.deleteMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
productId: args.productId,
|
||
},
|
||
});
|
||
|
||
// Возвращаем обновленный список избранного
|
||
const favorites = await prisma.favorites.findMany({
|
||
where: { organizationId: currentUser.organization.id },
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: {
|
||
include: {
|
||
users: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: "desc" },
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Товар удален из избранного",
|
||
favorites: favorites.map((favorite) => favorite.product),
|
||
};
|
||
} catch (error) {
|
||
console.error("Error removing from favorites:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при удалении из избранного",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Создать сотрудника
|
||
createEmployee: async (
|
||
_: unknown,
|
||
args: { input: CreateEmployeeInput },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
if (currentUser.organization.type !== "FULFILLMENT") {
|
||
throw new GraphQLError("Доступно только для фулфилмент центров");
|
||
}
|
||
|
||
try {
|
||
const employee = await prisma.employee.create({
|
||
data: {
|
||
...args.input,
|
||
organizationId: currentUser.organization.id,
|
||
birthDate: args.input.birthDate
|
||
? new Date(args.input.birthDate)
|
||
: undefined,
|
||
passportDate: args.input.passportDate
|
||
? new Date(args.input.passportDate)
|
||
: undefined,
|
||
hireDate: new Date(args.input.hireDate),
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Сотрудник успешно добавлен",
|
||
employee,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error creating employee:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при создании сотрудника",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Обновить сотрудника
|
||
updateEmployee: async (
|
||
_: unknown,
|
||
args: { id: string; input: UpdateEmployeeInput },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
if (currentUser.organization.type !== "FULFILLMENT") {
|
||
throw new GraphQLError("Доступно только для фулфилмент центров");
|
||
}
|
||
|
||
try {
|
||
const employee = await prisma.employee.update({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
data: {
|
||
...args.input,
|
||
birthDate: args.input.birthDate
|
||
? new Date(args.input.birthDate)
|
||
: undefined,
|
||
passportDate: args.input.passportDate
|
||
? new Date(args.input.passportDate)
|
||
: undefined,
|
||
hireDate: args.input.hireDate
|
||
? new Date(args.input.hireDate)
|
||
: undefined,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Сотрудник успешно обновлен",
|
||
employee,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error updating employee:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при обновлении сотрудника",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Удалить сотрудника
|
||
deleteEmployee: async (
|
||
_: unknown,
|
||
args: { id: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
if (currentUser.organization.type !== "FULFILLMENT") {
|
||
throw new GraphQLError("Доступно только для фулфилмент центров");
|
||
}
|
||
|
||
try {
|
||
await prisma.employee.delete({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
});
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Error deleting employee:", error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// Обновить табель сотрудника
|
||
updateEmployeeSchedule: async (
|
||
_: unknown,
|
||
args: { input: UpdateScheduleInput },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
if (currentUser.organization.type !== "FULFILLMENT") {
|
||
throw new GraphQLError("Доступно только для фулфилмент центров");
|
||
}
|
||
|
||
try {
|
||
// Проверяем что сотрудник принадлежит организации
|
||
const employee = await prisma.employee.findFirst({
|
||
where: {
|
||
id: args.input.employeeId,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
});
|
||
|
||
if (!employee) {
|
||
throw new GraphQLError("Сотрудник не найден");
|
||
}
|
||
|
||
// Создаем или обновляем запись табеля
|
||
await prisma.employeeSchedule.upsert({
|
||
where: {
|
||
employeeId_date: {
|
||
employeeId: args.input.employeeId,
|
||
date: new Date(args.input.date),
|
||
},
|
||
},
|
||
create: {
|
||
employeeId: args.input.employeeId,
|
||
date: new Date(args.input.date),
|
||
status: args.input.status,
|
||
hoursWorked: args.input.hoursWorked,
|
||
overtimeHours: args.input.overtimeHours,
|
||
notes: args.input.notes,
|
||
},
|
||
update: {
|
||
status: args.input.status,
|
||
hoursWorked: args.input.hoursWorked,
|
||
overtimeHours: args.input.overtimeHours,
|
||
notes: args.input.notes,
|
||
},
|
||
});
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Error updating employee schedule:", error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// Создать поставку Wildberries
|
||
createWildberriesSupply: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
cards: Array<{
|
||
price: number;
|
||
discountedPrice?: number;
|
||
selectedQuantity: number;
|
||
selectedServices?: string[];
|
||
}>;
|
||
};
|
||
},
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
// Пока что просто логируем данные, так как таблицы еще нет
|
||
console.log("Создание поставки Wildberries с данными:", args.input);
|
||
|
||
const totalAmount = args.input.cards.reduce((sum: number, card) => {
|
||
const cardPrice = card.discountedPrice || card.price;
|
||
const servicesPrice = (card.selectedServices?.length || 0) * 50;
|
||
return sum + (cardPrice + servicesPrice) * card.selectedQuantity;
|
||
}, 0);
|
||
|
||
const totalItems = args.input.cards.reduce(
|
||
(sum: number, card) => sum + card.selectedQuantity,
|
||
0
|
||
);
|
||
|
||
// Временная заглушка - вернем success без создания в БД
|
||
return {
|
||
success: true,
|
||
message: `Поставка создана успешно! Товаров: ${totalItems}, Сумма: ${totalAmount} руб.`,
|
||
supply: null, // Временно null
|
||
};
|
||
} catch (error) {
|
||
console.error("Error creating Wildberries supply:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при создании поставки Wildberries",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Создать поставщика для поставки
|
||
createSupplySupplier: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
name: string;
|
||
contactName: string;
|
||
phone: string;
|
||
market?: string;
|
||
address?: string;
|
||
place?: string;
|
||
telegram?: string;
|
||
};
|
||
},
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
// Создаем поставщика в базе данных
|
||
const supplier = await prisma.supplySupplier.create({
|
||
data: {
|
||
name: args.input.name,
|
||
contactName: args.input.contactName,
|
||
phone: args.input.phone,
|
||
market: args.input.market,
|
||
address: args.input.address,
|
||
place: args.input.place,
|
||
telegram: args.input.telegram,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Поставщик добавлен успешно!",
|
||
supplier: {
|
||
id: supplier.id,
|
||
name: supplier.name,
|
||
contactName: supplier.contactName,
|
||
phone: supplier.phone,
|
||
market: supplier.market,
|
||
address: supplier.address,
|
||
place: supplier.place,
|
||
telegram: supplier.telegram,
|
||
createdAt: supplier.createdAt,
|
||
},
|
||
};
|
||
} catch (error) {
|
||
console.error("Error creating supply supplier:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при добавлении поставщика",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Обновить статус заказа поставки
|
||
updateSupplyOrderStatus: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string;
|
||
status:
|
||
| "PENDING"
|
||
| "CONFIRMED"
|
||
| "IN_TRANSIT"
|
||
| "SUPPLIER_APPROVED"
|
||
| "LOGISTICS_CONFIRMED"
|
||
| "SHIPPED"
|
||
| "DELIVERED"
|
||
| "CANCELLED";
|
||
},
|
||
context: Context
|
||
) => {
|
||
console.log(`[DEBUG] updateSupplyOrderStatus вызван для заказа ${args.id} со статусом ${args.status}`);
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
// Находим заказ поставки
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
OR: [
|
||
{ organizationId: currentUser.organization.id }, // Создатель заказа
|
||
{ partnerId: currentUser.organization.id }, // Поставщик
|
||
{ fulfillmentCenterId: currentUser.organization.id }, // Фулфилмент-центр
|
||
],
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
partner: true,
|
||
fulfillmentCenter: true,
|
||
},
|
||
});
|
||
|
||
if (!existingOrder) {
|
||
throw new GraphQLError("Заказ поставки не найден или нет доступа");
|
||
}
|
||
|
||
// Обновляем статус заказа
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: args.status },
|
||
include: {
|
||
partner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
// ОТКЛЮЧЕНО: Устаревшая логика для обновления расходников
|
||
// Теперь используются специальные мутации для каждой роли
|
||
const targetOrganizationId = existingOrder.fulfillmentCenterId || existingOrder.organizationId;
|
||
|
||
if (args.status === "CONFIRMED") {
|
||
console.log(`[WARNING] Попытка использовать устаревший статус CONFIRMED для заказа ${args.id}`);
|
||
// Не обновляем расходники для устаревших статусов
|
||
// await prisma.supply.updateMany({
|
||
// where: {
|
||
// organizationId: targetOrganizationId,
|
||
// status: "planned",
|
||
// name: {
|
||
// in: existingOrder.items.map(item => item.product.name)
|
||
// }
|
||
// },
|
||
// data: {
|
||
// status: "confirmed"
|
||
// }
|
||
// });
|
||
|
||
console.log("✅ Статусы расходников обновлены на 'confirmed'");
|
||
}
|
||
|
||
if (args.status === "IN_TRANSIT") {
|
||
// При отгрузке - переводим расходники в статус "in-transit"
|
||
await prisma.supply.updateMany({
|
||
where: {
|
||
organizationId: targetOrganizationId,
|
||
status: "confirmed",
|
||
name: {
|
||
in: existingOrder.items.map(item => item.product.name)
|
||
}
|
||
},
|
||
data: {
|
||
status: "in-transit"
|
||
}
|
||
});
|
||
|
||
console.log("✅ Статусы расходников обновлены на 'in-transit'");
|
||
}
|
||
|
||
// Если статус изменился на DELIVERED, обновляем склад
|
||
if (args.status === "DELIVERED") {
|
||
|
||
console.log("🚚 Обновляем склад организации:", {
|
||
targetOrganizationId,
|
||
fulfillmentCenterId: existingOrder.fulfillmentCenterId,
|
||
organizationId: existingOrder.organizationId,
|
||
itemsCount: existingOrder.items.length,
|
||
items: existingOrder.items.map((item) => ({
|
||
productName: item.product.name,
|
||
quantity: item.quantity,
|
||
})),
|
||
});
|
||
|
||
// Обновляем расходники
|
||
for (const item of existingOrder.items) {
|
||
console.log("📦 Обрабатываем товар:", {
|
||
productName: item.product.name,
|
||
quantity: item.quantity,
|
||
targetOrganizationId,
|
||
});
|
||
|
||
// Ищем существующий расходник в правильной организации
|
||
const existingSupply = await prisma.supply.findFirst({
|
||
where: {
|
||
name: item.product.name,
|
||
organizationId: targetOrganizationId,
|
||
},
|
||
});
|
||
|
||
console.log("🔍 Найден существующий расходник:", !!existingSupply);
|
||
|
||
if (existingSupply) {
|
||
console.log("📈 Обновляем существующий расходник:", {
|
||
id: existingSupply.id,
|
||
oldStock: existingSupply.currentStock,
|
||
newStock: existingSupply.currentStock + item.quantity,
|
||
});
|
||
|
||
// Обновляем количество существующего расходника
|
||
await prisma.supply.update({
|
||
where: { id: existingSupply.id },
|
||
data: {
|
||
currentStock: existingSupply.currentStock + item.quantity,
|
||
status: "in-stock", // Меняем статус на "на складе"
|
||
},
|
||
});
|
||
} else {
|
||
console.log("➕ Создаем новый расходник:", {
|
||
name: item.product.name,
|
||
quantity: item.quantity,
|
||
organizationId: targetOrganizationId,
|
||
});
|
||
|
||
// Создаем новый расходник
|
||
const newSupply = await prisma.supply.create({
|
||
data: {
|
||
name: item.product.name,
|
||
description:
|
||
item.product.description ||
|
||
`Поставка от ${existingOrder.partner.name}`,
|
||
price: item.price,
|
||
quantity: item.quantity,
|
||
unit: "шт",
|
||
category: item.product.category?.name || "Расходники",
|
||
status: "in-stock",
|
||
date: new Date(),
|
||
supplier:
|
||
existingOrder.partner.name ||
|
||
existingOrder.partner.fullName ||
|
||
"Не указан",
|
||
minStock: Math.round(item.quantity * 0.1),
|
||
currentStock: item.quantity,
|
||
organizationId: targetOrganizationId,
|
||
},
|
||
});
|
||
|
||
console.log("✅ Создан новый расходник:", {
|
||
id: newSupply.id,
|
||
name: newSupply.name,
|
||
currentStock: newSupply.currentStock,
|
||
});
|
||
}
|
||
}
|
||
|
||
console.log("🎉 Склад организации успешно обновлен!");
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: `Статус заказа поставки обновлен на "${args.status}"`,
|
||
order: updatedOrder,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error updating supply order status:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при обновлении статуса заказа поставки",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Резолверы для новых действий с заказами поставок
|
||
supplierApproveOrder: async (
|
||
_: unknown,
|
||
args: { id: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что пользователь - поставщик этого заказа
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
partnerId: currentUser.organization.id, // Только поставщик может одобрить
|
||
status: "PENDING", // Можно одобрить только заказы в статусе PENDING
|
||
},
|
||
});
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: "Заказ не найден или недоступен для одобрения",
|
||
};
|
||
}
|
||
|
||
console.log(`[DEBUG] Поставщик ${currentUser.organization.name} одобряет заказ ${args.id}`);
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: "SUPPLIER_APPROVED" },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
console.log(`[DEBUG] Заказ ${args.id} успешно обновлен до статуса: ${updatedOrder.status}`);
|
||
return {
|
||
success: true,
|
||
message: "Заказ поставки одобрен поставщиком",
|
||
order: updatedOrder,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error approving supply order:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при одобрении заказа поставки",
|
||
};
|
||
}
|
||
},
|
||
|
||
supplierRejectOrder: async (
|
||
_: unknown,
|
||
args: { id: string; reason?: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
partnerId: currentUser.organization.id,
|
||
status: "PENDING",
|
||
},
|
||
});
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: "Заказ не найден или недоступен для отклонения",
|
||
};
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: "CANCELLED" },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: args.reason
|
||
? `Заказ отклонен поставщиком. Причина: ${args.reason}`
|
||
: "Заказ отклонен поставщиком",
|
||
order: updatedOrder,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error rejecting supply order:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при отклонении заказа поставки",
|
||
};
|
||
}
|
||
},
|
||
|
||
supplierShipOrder: async (
|
||
_: unknown,
|
||
args: { id: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
partnerId: currentUser.organization.id,
|
||
status: "LOGISTICS_CONFIRMED",
|
||
},
|
||
});
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: "Заказ не найден или недоступен для отправки",
|
||
};
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: "SHIPPED" },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Заказ отправлен поставщиком",
|
||
order: updatedOrder,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error shipping supply order:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при отправке заказа поставки",
|
||
};
|
||
}
|
||
},
|
||
|
||
logisticsConfirmOrder: async (
|
||
_: unknown,
|
||
args: { id: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
logisticsPartnerId: currentUser.organization.id,
|
||
status: "SUPPLIER_APPROVED",
|
||
},
|
||
});
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: "Заказ не найден или недоступен для подтверждения логистикой",
|
||
};
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: "LOGISTICS_CONFIRMED" },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Заказ подтвержден логистической компанией",
|
||
order: updatedOrder,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error confirming supply order:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при подтверждении заказа логистикой",
|
||
};
|
||
}
|
||
},
|
||
|
||
logisticsRejectOrder: async (
|
||
_: unknown,
|
||
args: { id: string; reason?: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
logisticsPartnerId: currentUser.organization.id,
|
||
status: "SUPPLIER_APPROVED",
|
||
},
|
||
});
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: "Заказ не найден или недоступен для отклонения логистикой",
|
||
};
|
||
}
|
||
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: "CANCELLED" },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: args.reason
|
||
? `Заказ отклонен логистической компанией. Причина: ${args.reason}`
|
||
: "Заказ отклонен логистической компанией",
|
||
order: updatedOrder,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error rejecting supply order:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при отклонении заказа логистикой",
|
||
};
|
||
}
|
||
},
|
||
|
||
fulfillmentReceiveOrder: async (
|
||
_: unknown,
|
||
args: { id: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
const existingOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
fulfillmentCenterId: currentUser.organization.id,
|
||
status: "SHIPPED",
|
||
},
|
||
include: {
|
||
items: {
|
||
include: {
|
||
product: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
if (!existingOrder) {
|
||
return {
|
||
success: false,
|
||
message: "Заказ не найден или недоступен для приема",
|
||
};
|
||
}
|
||
|
||
// Обновляем статус заказа
|
||
const updatedOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.id },
|
||
data: { status: "DELIVERED" },
|
||
include: {
|
||
partner: true,
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: {
|
||
include: {
|
||
category: true,
|
||
organization: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
// Обновляем склад фулфилмента
|
||
for (const item of existingOrder.items) {
|
||
const existingSupply = await prisma.supply.findFirst({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
name: item.product.name,
|
||
},
|
||
});
|
||
|
||
if (existingSupply) {
|
||
await prisma.supply.update({
|
||
where: { id: existingSupply.id },
|
||
data: {
|
||
currentStock: existingSupply.currentStock + item.quantity,
|
||
quantity: existingSupply.quantity + item.quantity,
|
||
status: "in-stock",
|
||
},
|
||
});
|
||
} else {
|
||
await prisma.supply.create({
|
||
data: {
|
||
name: item.product.name,
|
||
description: item.product.description || `Расходники от ${updatedOrder.partner.name}`,
|
||
price: item.price,
|
||
quantity: item.quantity,
|
||
currentStock: item.quantity,
|
||
usedStock: 0,
|
||
unit: "шт",
|
||
category: item.product.category?.name || "Расходники",
|
||
status: "in-stock",
|
||
supplier: updatedOrder.partner.name || updatedOrder.partner.fullName || "Поставщик",
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: "Заказ принят фулфилментом. Склад обновлен.",
|
||
order: updatedOrder,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error receiving supply order:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при приеме заказа поставки",
|
||
};
|
||
}
|
||
},
|
||
|
||
updateExternalAdClicks: async (
|
||
_: unknown,
|
||
{ id, clicks }: { id: string; clicks: number },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
// Проверяем, что реклама принадлежит организации пользователя
|
||
const existingAd = await prisma.externalAd.findFirst({
|
||
where: {
|
||
id,
|
||
organizationId: user.organization.id,
|
||
},
|
||
});
|
||
|
||
if (!existingAd) {
|
||
throw new GraphQLError("Внешняя реклама не найдена");
|
||
}
|
||
|
||
await prisma.externalAd.update({
|
||
where: { id },
|
||
data: { clicks },
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Клики успешно обновлены",
|
||
externalAd: null,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error updating external ad clicks:", error);
|
||
return {
|
||
success: false,
|
||
message:
|
||
error instanceof Error ? error.message : "Ошибка обновления кликов",
|
||
externalAd: null,
|
||
};
|
||
}
|
||
},
|
||
},
|
||
|
||
// Резолверы типов
|
||
Organization: {
|
||
users: async (parent: { id: string; users?: unknown[] }) => {
|
||
// Если пользователи уже загружены через include, возвращаем их
|
||
if (parent.users) {
|
||
return parent.users;
|
||
}
|
||
|
||
// Иначе загружаем отдельно
|
||
return await prisma.user.findMany({
|
||
where: { organizationId: parent.id },
|
||
});
|
||
},
|
||
services: async (parent: { id: string; services?: unknown[] }) => {
|
||
// Если услуги уже загружены через include, возвращаем их
|
||
if (parent.services) {
|
||
return parent.services;
|
||
}
|
||
|
||
// Иначе загружаем отдельно
|
||
return await prisma.service.findMany({
|
||
where: { organizationId: parent.id },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: "desc" },
|
||
});
|
||
},
|
||
supplies: async (parent: { id: string; supplies?: unknown[] }) => {
|
||
// Если расходники уже загружены через include, возвращаем их
|
||
if (parent.supplies) {
|
||
return parent.supplies;
|
||
}
|
||
|
||
// Иначе загружаем отдельно
|
||
return await prisma.supply.findMany({
|
||
where: { organizationId: parent.id },
|
||
include: { organization: true },
|
||
orderBy: { createdAt: "desc" },
|
||
});
|
||
},
|
||
},
|
||
|
||
Cart: {
|
||
totalPrice: (parent: {
|
||
items: Array<{ product: { price: number }; quantity: number }>;
|
||
}) => {
|
||
return parent.items.reduce((total, item) => {
|
||
return total + Number(item.product.price) * item.quantity;
|
||
}, 0);
|
||
},
|
||
totalItems: (parent: { items: Array<{ quantity: number }> }) => {
|
||
return parent.items.reduce((total, item) => total + item.quantity, 0);
|
||
},
|
||
},
|
||
|
||
CartItem: {
|
||
totalPrice: (parent: { product: { price: number }; quantity: number }) => {
|
||
return Number(parent.product.price) * parent.quantity;
|
||
},
|
||
isAvailable: (parent: {
|
||
product: { quantity: number; isActive: boolean };
|
||
quantity: number;
|
||
}) => {
|
||
return (
|
||
parent.product.isActive && parent.product.quantity >= parent.quantity
|
||
);
|
||
},
|
||
availableQuantity: (parent: { product: { quantity: number } }) => {
|
||
return parent.product.quantity;
|
||
},
|
||
},
|
||
|
||
User: {
|
||
organization: async (parent: {
|
||
organizationId?: string;
|
||
organization?: unknown;
|
||
}) => {
|
||
// Если организация уже загружена через include, возвращаем её
|
||
if (parent.organization) {
|
||
return parent.organization;
|
||
}
|
||
|
||
// Иначе загружаем отдельно если есть organizationId
|
||
if (parent.organizationId) {
|
||
return await prisma.organization.findUnique({
|
||
where: { id: parent.organizationId },
|
||
include: {
|
||
apiKeys: true,
|
||
users: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
return null;
|
||
},
|
||
},
|
||
|
||
Product: {
|
||
type: (parent: { type?: string | null }) => parent.type || "PRODUCT",
|
||
images: (parent: { images: unknown }) => {
|
||
// Если images это строка JSON, парсим её в массив
|
||
if (typeof parent.images === "string") {
|
||
try {
|
||
return JSON.parse(parent.images);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
// Если это уже массив, возвращаем как есть
|
||
if (Array.isArray(parent.images)) {
|
||
return parent.images;
|
||
}
|
||
// Иначе возвращаем пустой массив
|
||
return [];
|
||
},
|
||
},
|
||
|
||
Message: {
|
||
type: (parent: { type?: string | null }) => {
|
||
return parent.type || "TEXT";
|
||
},
|
||
createdAt: (parent: { createdAt: Date | string }) => {
|
||
if (parent.createdAt instanceof Date) {
|
||
return parent.createdAt.toISOString();
|
||
}
|
||
return parent.createdAt;
|
||
},
|
||
updatedAt: (parent: { updatedAt: Date | string }) => {
|
||
if (parent.updatedAt instanceof Date) {
|
||
return parent.updatedAt.toISOString();
|
||
}
|
||
return parent.updatedAt;
|
||
},
|
||
},
|
||
|
||
Employee: {
|
||
birthDate: (parent: { birthDate?: Date | string | null }) => {
|
||
if (!parent.birthDate) return null;
|
||
if (parent.birthDate instanceof Date) {
|
||
return parent.birthDate.toISOString();
|
||
}
|
||
return parent.birthDate;
|
||
},
|
||
passportDate: (parent: { passportDate?: Date | string | null }) => {
|
||
if (!parent.passportDate) return null;
|
||
if (parent.passportDate instanceof Date) {
|
||
return parent.passportDate.toISOString();
|
||
}
|
||
return parent.passportDate;
|
||
},
|
||
hireDate: (parent: { hireDate: Date | string }) => {
|
||
if (parent.hireDate instanceof Date) {
|
||
return parent.hireDate.toISOString();
|
||
}
|
||
return parent.hireDate;
|
||
},
|
||
createdAt: (parent: { createdAt: Date | string }) => {
|
||
if (parent.createdAt instanceof Date) {
|
||
return parent.createdAt.toISOString();
|
||
}
|
||
return parent.createdAt;
|
||
},
|
||
updatedAt: (parent: { updatedAt: Date | string }) => {
|
||
if (parent.updatedAt instanceof Date) {
|
||
return parent.updatedAt.toISOString();
|
||
}
|
||
return parent.updatedAt;
|
||
},
|
||
},
|
||
|
||
EmployeeSchedule: {
|
||
date: (parent: { date: Date | string }) => {
|
||
if (parent.date instanceof Date) {
|
||
return parent.date.toISOString();
|
||
}
|
||
return parent.date;
|
||
},
|
||
createdAt: (parent: { createdAt: Date | string }) => {
|
||
if (parent.createdAt instanceof Date) {
|
||
return parent.createdAt.toISOString();
|
||
}
|
||
return parent.createdAt;
|
||
},
|
||
updatedAt: (parent: { updatedAt: Date | string }) => {
|
||
if (parent.updatedAt instanceof Date) {
|
||
return parent.updatedAt.toISOString();
|
||
}
|
||
return parent.updatedAt;
|
||
},
|
||
employee: async (parent: { employeeId: string }) => {
|
||
return await prisma.employee.findUnique({
|
||
where: { id: parent.employeeId },
|
||
});
|
||
},
|
||
},
|
||
};
|
||
|
||
// Мутации для категорий
|
||
const categoriesMutations = {
|
||
// Создать категорию
|
||
createCategory: async (_: unknown, args: { input: { name: string } }) => {
|
||
try {
|
||
// Проверяем есть ли уже категория с таким именем
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { name: args.input.name },
|
||
});
|
||
|
||
if (existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: "Категория с таким названием уже существует",
|
||
};
|
||
}
|
||
|
||
const category = await prisma.category.create({
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Категория успешно создана",
|
||
category,
|
||
};
|
||
} catch (error) {
|
||
console.error("Ошибка создания категории:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при создании категории",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Обновить категорию
|
||
updateCategory: async (
|
||
_: unknown,
|
||
args: { id: string; input: { name: string } }
|
||
) => {
|
||
try {
|
||
// Проверяем существует ли категория
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
});
|
||
|
||
if (!existingCategory) {
|
||
return {
|
||
success: false,
|
||
message: "Категория не найдена",
|
||
};
|
||
}
|
||
|
||
// Проверяем не занято ли имя другой категорией
|
||
const duplicateCategory = await prisma.category.findFirst({
|
||
where: {
|
||
name: args.input.name,
|
||
id: { not: args.id },
|
||
},
|
||
});
|
||
|
||
if (duplicateCategory) {
|
||
return {
|
||
success: false,
|
||
message: "Категория с таким названием уже существует",
|
||
};
|
||
}
|
||
|
||
const category = await prisma.category.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
name: args.input.name,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Категория успешно обновлена",
|
||
category,
|
||
};
|
||
} catch (error) {
|
||
console.error("Ошибка обновления категории:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при обновлении категории",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Удалить категорию
|
||
deleteCategory: async (_: unknown, args: { id: string }) => {
|
||
try {
|
||
// Проверяем существует ли категория
|
||
const existingCategory = await prisma.category.findUnique({
|
||
where: { id: args.id },
|
||
});
|
||
|
||
if (!existingCategory) {
|
||
throw new GraphQLError("Категория не найдена");
|
||
}
|
||
|
||
// Проверяем есть ли товары в этой категории
|
||
const productsCount = await prisma.product.count({
|
||
where: { categoryId: args.id },
|
||
});
|
||
|
||
if (productsCount > 0) {
|
||
throw new GraphQLError(
|
||
"Нельзя удалить категорию, в которой есть товары"
|
||
);
|
||
}
|
||
|
||
await prisma.category.delete({
|
||
where: { id: args.id },
|
||
});
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Ошибка удаления категории:", error);
|
||
if (error instanceof GraphQLError) {
|
||
throw error;
|
||
}
|
||
throw new GraphQLError("Ошибка при удалении категории");
|
||
}
|
||
},
|
||
};
|
||
|
||
// Логистические мутации
|
||
const logisticsMutations = {
|
||
// Создать логистический маршрут
|
||
createLogistics: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
fromLocation: string;
|
||
toLocation: string;
|
||
priceUnder1m3: number;
|
||
priceOver1m3: number;
|
||
description?: string;
|
||
};
|
||
},
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
const logistics = await prisma.logistics.create({
|
||
data: {
|
||
fromLocation: args.input.fromLocation,
|
||
toLocation: args.input.toLocation,
|
||
priceUnder1m3: args.input.priceUnder1m3,
|
||
priceOver1m3: args.input.priceOver1m3,
|
||
description: args.input.description,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
console.log("✅ Logistics created:", logistics.id);
|
||
|
||
return {
|
||
success: true,
|
||
message: "Логистический маршрут создан",
|
||
logistics,
|
||
};
|
||
} catch (error) {
|
||
console.error("❌ Error creating logistics:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при создании логистического маршрута",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Обновить логистический маршрут
|
||
updateLogistics: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string;
|
||
input: {
|
||
fromLocation: string;
|
||
toLocation: string;
|
||
priceUnder1m3: number;
|
||
priceOver1m3: number;
|
||
description?: string;
|
||
};
|
||
},
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что маршрут принадлежит организации пользователя
|
||
const existingLogistics = await prisma.logistics.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
});
|
||
|
||
if (!existingLogistics) {
|
||
throw new GraphQLError("Логистический маршрут не найден");
|
||
}
|
||
|
||
const logistics = await prisma.logistics.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
fromLocation: args.input.fromLocation,
|
||
toLocation: args.input.toLocation,
|
||
priceUnder1m3: args.input.priceUnder1m3,
|
||
priceOver1m3: args.input.priceOver1m3,
|
||
description: args.input.description,
|
||
},
|
||
include: {
|
||
organization: true,
|
||
},
|
||
});
|
||
|
||
console.log("✅ Logistics updated:", logistics.id);
|
||
|
||
return {
|
||
success: true,
|
||
message: "Логистический маршрут обновлен",
|
||
logistics,
|
||
};
|
||
} catch (error) {
|
||
console.error("❌ Error updating logistics:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка при обновлении логистического маршрута",
|
||
};
|
||
}
|
||
},
|
||
|
||
// Удалить логистический маршрут
|
||
deleteLogistics: async (
|
||
_: unknown,
|
||
args: { id: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError("У пользователя нет организации");
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что маршрут принадлежит организации пользователя
|
||
const existingLogistics = await prisma.logistics.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
});
|
||
|
||
if (!existingLogistics) {
|
||
throw new GraphQLError("Логистический маршрут не найден");
|
||
}
|
||
|
||
await prisma.logistics.delete({
|
||
where: { id: args.id },
|
||
});
|
||
|
||
console.log("✅ Logistics deleted:", args.id);
|
||
return true;
|
||
} catch (error) {
|
||
console.error("❌ Error deleting logistics:", error);
|
||
return false;
|
||
}
|
||
},
|
||
};
|
||
|
||
// Добавляем дополнительные мутации к основным резолверам
|
||
resolvers.Mutation = {
|
||
...resolvers.Mutation,
|
||
...categoriesMutations,
|
||
...logisticsMutations,
|
||
};
|
||
|
||
// Админ резолверы
|
||
const adminQueries = {
|
||
adminMe: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.admin) {
|
||
throw new GraphQLError("Требуется авторизация администратора", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const admin = await prisma.admin.findUnique({
|
||
where: { id: context.admin.id },
|
||
});
|
||
|
||
if (!admin) {
|
||
throw new GraphQLError("Администратор не найден");
|
||
}
|
||
|
||
return admin;
|
||
},
|
||
|
||
allUsers: async (
|
||
_: unknown,
|
||
args: { search?: string; limit?: number; offset?: number },
|
||
context: Context
|
||
) => {
|
||
if (!context.admin) {
|
||
throw new GraphQLError("Требуется авторизация администратора", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
const limit = args.limit || 50;
|
||
const offset = args.offset || 0;
|
||
|
||
// Строим условие поиска
|
||
const whereCondition: Prisma.UserWhereInput = args.search
|
||
? {
|
||
OR: [
|
||
{ phone: { contains: args.search, mode: "insensitive" } },
|
||
{ managerName: { contains: args.search, mode: "insensitive" } },
|
||
{
|
||
organization: {
|
||
OR: [
|
||
{ name: { contains: args.search, mode: "insensitive" } },
|
||
{ fullName: { contains: args.search, mode: "insensitive" } },
|
||
{ inn: { contains: args.search, mode: "insensitive" } },
|
||
],
|
||
},
|
||
},
|
||
],
|
||
}
|
||
: {};
|
||
|
||
// Получаем пользователей с пагинацией
|
||
const [users, total] = await Promise.all([
|
||
prisma.user.findMany({
|
||
where: whereCondition,
|
||
include: {
|
||
organization: true,
|
||
},
|
||
take: limit,
|
||
skip: offset,
|
||
orderBy: { createdAt: "desc" },
|
||
}),
|
||
prisma.user.count({
|
||
where: whereCondition,
|
||
}),
|
||
]);
|
||
|
||
return {
|
||
users,
|
||
total,
|
||
hasMore: offset + limit < total,
|
||
};
|
||
},
|
||
};
|
||
|
||
const adminMutations = {
|
||
adminLogin: async (
|
||
_: unknown,
|
||
args: { username: string; password: string }
|
||
) => {
|
||
try {
|
||
// Найти администратора
|
||
const admin = await prisma.admin.findUnique({
|
||
where: { username: args.username },
|
||
});
|
||
|
||
if (!admin) {
|
||
return {
|
||
success: false,
|
||
message: "Неверные учетные данные",
|
||
};
|
||
}
|
||
|
||
// Проверить активность
|
||
if (!admin.isActive) {
|
||
return {
|
||
success: false,
|
||
message: "Аккаунт заблокирован",
|
||
};
|
||
}
|
||
|
||
// Проверить пароль
|
||
const isPasswordValid = await bcrypt.compare(
|
||
args.password,
|
||
admin.password
|
||
);
|
||
|
||
if (!isPasswordValid) {
|
||
return {
|
||
success: false,
|
||
message: "Неверные учетные данные",
|
||
};
|
||
}
|
||
|
||
// Обновить время последнего входа
|
||
await prisma.admin.update({
|
||
where: { id: admin.id },
|
||
data: { lastLogin: new Date() },
|
||
});
|
||
|
||
// Создать токен
|
||
const token = jwt.sign(
|
||
{
|
||
adminId: admin.id,
|
||
username: admin.username,
|
||
type: "admin",
|
||
},
|
||
process.env.JWT_SECRET!,
|
||
{ expiresIn: "24h" }
|
||
);
|
||
|
||
return {
|
||
success: true,
|
||
message: "Успешная авторизация",
|
||
token,
|
||
admin: {
|
||
...admin,
|
||
password: undefined, // Не возвращаем пароль
|
||
},
|
||
};
|
||
} catch (error) {
|
||
console.error("Admin login error:", error);
|
||
return {
|
||
success: false,
|
||
message: "Ошибка авторизации",
|
||
};
|
||
}
|
||
},
|
||
|
||
adminLogout: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.admin) {
|
||
throw new GraphQLError("Требуется авторизация администратора", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
return true;
|
||
},
|
||
};
|
||
|
||
// Wildberries статистика
|
||
const wildberriesQueries = {
|
||
debugWildberriesAdverts: async (
|
||
_: unknown,
|
||
__: unknown,
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
if (!user?.organization || user.organization.type !== "SELLER") {
|
||
throw new GraphQLError("Доступно только для продавцов");
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find(
|
||
(key) => key.marketplace === "WILDBERRIES" && key.isActive
|
||
);
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError("WB API ключ не настроен");
|
||
}
|
||
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey);
|
||
|
||
// Получаем кампании во всех статусах
|
||
const [active, completed, paused] = await Promise.all([
|
||
wbService.getAdverts(9).catch(() => []), // активные
|
||
wbService.getAdverts(7).catch(() => []), // завершенные
|
||
wbService.getAdverts(11).catch(() => []), // на паузе
|
||
]);
|
||
|
||
const allCampaigns = [...active, ...completed, ...paused];
|
||
|
||
return {
|
||
success: true,
|
||
message: `Found ${active.length} active, ${completed.length} completed, ${paused.length} paused campaigns`,
|
||
campaignsCount: allCampaigns.length,
|
||
campaigns: allCampaigns.map((c) => ({
|
||
id: c.advertId,
|
||
name: c.name,
|
||
status: c.status,
|
||
type: c.type,
|
||
})),
|
||
};
|
||
} catch (error) {
|
||
console.error("Error debugging WB adverts:", error);
|
||
return {
|
||
success: false,
|
||
message: error instanceof Error ? error.message : "Unknown error",
|
||
campaignsCount: 0,
|
||
campaigns: [],
|
||
};
|
||
}
|
||
},
|
||
|
||
getWildberriesStatistics: async (
|
||
_: unknown,
|
||
{
|
||
period,
|
||
startDate,
|
||
endDate,
|
||
}: {
|
||
period?: "week" | "month" | "quarter";
|
||
startDate?: string;
|
||
endDate?: string;
|
||
},
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
// Получаем организацию пользователя и её WB API ключ
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
if (user.organization.type !== "SELLER") {
|
||
throw new GraphQLError("Доступно только для продавцов");
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find(
|
||
(key) => key.marketplace === "WILDBERRIES" && key.isActive
|
||
);
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError("WB API ключ не настроен");
|
||
}
|
||
|
||
// Создаем экземпляр сервиса
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey);
|
||
|
||
// Получаем даты
|
||
let dateFrom: string;
|
||
let dateTo: string;
|
||
|
||
if (startDate && endDate) {
|
||
// Используем пользовательские даты
|
||
dateFrom = startDate;
|
||
dateTo = endDate;
|
||
} else if (period) {
|
||
// Используем предустановленный период
|
||
dateFrom = WildberriesService.getDatePeriodAgo(period);
|
||
dateTo = WildberriesService.formatDate(new Date());
|
||
} else {
|
||
throw new GraphQLError(
|
||
"Необходимо указать либо period, либо startDate и endDate"
|
||
);
|
||
}
|
||
|
||
// Получаем статистику
|
||
const statistics = await wbService.getStatistics(dateFrom, dateTo);
|
||
|
||
return {
|
||
success: true,
|
||
data: statistics,
|
||
message: null,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error fetching WB statistics:", error);
|
||
return {
|
||
success: false,
|
||
message:
|
||
error instanceof Error
|
||
? error.message
|
||
: "Ошибка получения статистики",
|
||
data: [],
|
||
};
|
||
}
|
||
},
|
||
|
||
getWildberriesCampaignStats: async (
|
||
_: unknown,
|
||
{
|
||
input,
|
||
}: {
|
||
input: {
|
||
campaigns: Array<{
|
||
id: number;
|
||
dates?: string[];
|
||
interval?: {
|
||
begin: string;
|
||
end: string;
|
||
};
|
||
}>;
|
||
};
|
||
},
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
// Получаем организацию пользователя и её WB API ключ
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
if (user.organization.type !== "SELLER") {
|
||
throw new GraphQLError("Доступно только для продавцов");
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find(
|
||
(key) => key.marketplace === "WILDBERRIES" && key.isActive
|
||
);
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError("WB API ключ не настроен");
|
||
}
|
||
|
||
// Создаем экземпляр сервиса
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey);
|
||
|
||
// Преобразуем запросы в нужный формат
|
||
const requests = input.campaigns.map((campaign) => {
|
||
if (campaign.dates && campaign.dates.length > 0) {
|
||
return {
|
||
id: campaign.id,
|
||
dates: campaign.dates,
|
||
};
|
||
} else if (campaign.interval) {
|
||
return {
|
||
id: campaign.id,
|
||
interval: campaign.interval,
|
||
};
|
||
} else {
|
||
// Если не указаны ни даты, ни интервал, возвращаем данные только за последние сутки
|
||
return {
|
||
id: campaign.id,
|
||
};
|
||
}
|
||
});
|
||
|
||
// Получаем статистику кампаний
|
||
const campaignStats = await wbService.getCampaignStats(requests);
|
||
|
||
return {
|
||
success: true,
|
||
data: campaignStats,
|
||
message: null,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error fetching WB campaign stats:", error);
|
||
return {
|
||
success: false,
|
||
message:
|
||
error instanceof Error
|
||
? error.message
|
||
: "Ошибка получения статистики кампаний",
|
||
data: [],
|
||
};
|
||
}
|
||
},
|
||
|
||
getWildberriesCampaignsList: async (
|
||
_: unknown,
|
||
__: unknown,
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
// Получаем организацию пользователя и её WB API ключ
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: {
|
||
organization: {
|
||
include: {
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
if (user.organization.type !== "SELLER") {
|
||
throw new GraphQLError("Доступно только для продавцов");
|
||
}
|
||
|
||
const wbApiKeyRecord = user.organization.apiKeys?.find(
|
||
(key) => key.marketplace === "WILDBERRIES" && key.isActive
|
||
);
|
||
|
||
if (!wbApiKeyRecord) {
|
||
throw new GraphQLError("WB API ключ не настроен");
|
||
}
|
||
|
||
// Создаем экземпляр сервиса
|
||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey);
|
||
|
||
// Получаем список кампаний
|
||
const campaignsList = await wbService.getCampaignsList();
|
||
|
||
return {
|
||
success: true,
|
||
data: campaignsList,
|
||
message: null,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error fetching WB campaigns list:", error);
|
||
return {
|
||
success: false,
|
||
message:
|
||
error instanceof Error
|
||
? error.message
|
||
: "Ошибка получения списка кампаний",
|
||
data: {
|
||
adverts: [],
|
||
all: 0,
|
||
},
|
||
};
|
||
}
|
||
},
|
||
};
|
||
|
||
// Резолверы для внешней рекламы
|
||
const externalAdQueries = {
|
||
getExternalAds: async (
|
||
_: unknown,
|
||
{ dateFrom, dateTo }: { dateFrom: string; dateTo: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
const externalAds = await prisma.externalAd.findMany({
|
||
where: {
|
||
organizationId: user.organization.id,
|
||
date: {
|
||
gte: new Date(dateFrom),
|
||
lte: new Date(dateTo + "T23:59:59.999Z"),
|
||
},
|
||
},
|
||
orderBy: {
|
||
date: "desc",
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: null,
|
||
externalAds: externalAds.map((ad) => ({
|
||
...ad,
|
||
cost: parseFloat(ad.cost.toString()),
|
||
date: ad.date.toISOString().split("T")[0],
|
||
createdAt: ad.createdAt.toISOString(),
|
||
updatedAt: ad.updatedAt.toISOString(),
|
||
})),
|
||
};
|
||
} catch (error) {
|
||
console.error("Error fetching external ads:", error);
|
||
return {
|
||
success: false,
|
||
message:
|
||
error instanceof Error
|
||
? error.message
|
||
: "Ошибка получения внешней рекламы",
|
||
externalAds: [],
|
||
};
|
||
}
|
||
},
|
||
};
|
||
|
||
const externalAdMutations = {
|
||
createExternalAd: async (
|
||
_: unknown,
|
||
{
|
||
input,
|
||
}: {
|
||
input: {
|
||
name: string;
|
||
url: string;
|
||
cost: number;
|
||
date: string;
|
||
nmId: string;
|
||
};
|
||
},
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
const externalAd = await prisma.externalAd.create({
|
||
data: {
|
||
name: input.name,
|
||
url: input.url,
|
||
cost: input.cost,
|
||
date: new Date(input.date),
|
||
nmId: input.nmId,
|
||
organizationId: user.organization.id,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Внешняя реклама успешно создана",
|
||
externalAd: {
|
||
...externalAd,
|
||
cost: parseFloat(externalAd.cost.toString()),
|
||
date: externalAd.date.toISOString().split("T")[0],
|
||
createdAt: externalAd.createdAt.toISOString(),
|
||
updatedAt: externalAd.updatedAt.toISOString(),
|
||
},
|
||
};
|
||
} catch (error) {
|
||
console.error("Error creating external ad:", error);
|
||
return {
|
||
success: false,
|
||
message:
|
||
error instanceof Error
|
||
? error.message
|
||
: "Ошибка создания внешней рекламы",
|
||
externalAd: null,
|
||
};
|
||
}
|
||
},
|
||
|
||
updateExternalAd: async (
|
||
_: unknown,
|
||
{
|
||
id,
|
||
input,
|
||
}: {
|
||
id: string;
|
||
input: {
|
||
name: string;
|
||
url: string;
|
||
cost: number;
|
||
date: string;
|
||
nmId: string;
|
||
};
|
||
},
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
// Проверяем, что реклама принадлежит организации пользователя
|
||
const existingAd = await prisma.externalAd.findFirst({
|
||
where: {
|
||
id,
|
||
organizationId: user.organization.id,
|
||
},
|
||
});
|
||
|
||
if (!existingAd) {
|
||
throw new GraphQLError("Внешняя реклама не найдена");
|
||
}
|
||
|
||
const externalAd = await prisma.externalAd.update({
|
||
where: { id },
|
||
data: {
|
||
name: input.name,
|
||
url: input.url,
|
||
cost: input.cost,
|
||
date: new Date(input.date),
|
||
nmId: input.nmId,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Внешняя реклама успешно обновлена",
|
||
externalAd: {
|
||
...externalAd,
|
||
cost: parseFloat(externalAd.cost.toString()),
|
||
date: externalAd.date.toISOString().split("T")[0],
|
||
createdAt: externalAd.createdAt.toISOString(),
|
||
updatedAt: externalAd.updatedAt.toISOString(),
|
||
},
|
||
};
|
||
} catch (error) {
|
||
console.error("Error updating external ad:", error);
|
||
return {
|
||
success: false,
|
||
message:
|
||
error instanceof Error
|
||
? error.message
|
||
: "Ошибка обновления внешней рекламы",
|
||
externalAd: null,
|
||
};
|
||
}
|
||
},
|
||
|
||
deleteExternalAd: async (
|
||
_: unknown,
|
||
{ id }: { id: string },
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
// Проверяем, что реклама принадлежит организации пользователя
|
||
const existingAd = await prisma.externalAd.findFirst({
|
||
where: {
|
||
id,
|
||
organizationId: user.organization.id,
|
||
},
|
||
});
|
||
|
||
if (!existingAd) {
|
||
throw new GraphQLError("Внешняя реклама не найдена");
|
||
}
|
||
|
||
await prisma.externalAd.delete({
|
||
where: { id },
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Внешняя реклама успешно удалена",
|
||
externalAd: null,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error deleting external ad:", error);
|
||
return {
|
||
success: false,
|
||
message:
|
||
error instanceof Error
|
||
? error.message
|
||
: "Ошибка удаления внешней рекламы",
|
||
externalAd: null,
|
||
};
|
||
}
|
||
},
|
||
};
|
||
|
||
// Резолверы для кеша склада WB
|
||
const wbWarehouseCacheQueries = {
|
||
getWBWarehouseData: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
// Получаем текущую дату без времени
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
// Ищем кеш за сегодня
|
||
const cache = await prisma.wBWarehouseCache.findFirst({
|
||
where: {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
},
|
||
orderBy: {
|
||
createdAt: "desc",
|
||
},
|
||
});
|
||
|
||
if (cache) {
|
||
// Возвращаем данные из кеша
|
||
return {
|
||
success: true,
|
||
message: "Данные получены из кеша",
|
||
cache: {
|
||
...cache,
|
||
cacheDate: cache.cacheDate.toISOString().split("T")[0],
|
||
createdAt: cache.createdAt.toISOString(),
|
||
updatedAt: cache.updatedAt.toISOString(),
|
||
},
|
||
fromCache: true,
|
||
};
|
||
} else {
|
||
// Кеша нет, нужно загрузить данные из API
|
||
return {
|
||
success: true,
|
||
message: "Кеш не найден, требуется загрузка из API",
|
||
cache: null,
|
||
fromCache: false,
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.error("Error getting WB warehouse cache:", error);
|
||
return {
|
||
success: false,
|
||
message:
|
||
error instanceof Error
|
||
? error.message
|
||
: "Ошибка получения кеша склада WB",
|
||
cache: null,
|
||
fromCache: false,
|
||
};
|
||
}
|
||
},
|
||
};
|
||
|
||
const wbWarehouseCacheMutations = {
|
||
saveWBWarehouseCache: async (
|
||
_: unknown,
|
||
{
|
||
input,
|
||
}: {
|
||
input: {
|
||
data: string;
|
||
totalProducts: number;
|
||
totalStocks: number;
|
||
totalReserved: number;
|
||
};
|
||
},
|
||
context: Context
|
||
) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError("Требуется авторизация", {
|
||
extensions: { code: "UNAUTHENTICATED" },
|
||
});
|
||
}
|
||
|
||
try {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
});
|
||
|
||
if (!user?.organization) {
|
||
throw new GraphQLError("Организация не найдена");
|
||
}
|
||
|
||
// Получаем текущую дату без времени
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
// Используем upsert для создания или обновления кеша
|
||
const cache = await prisma.wBWarehouseCache.upsert({
|
||
where: {
|
||
organizationId_cacheDate: {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
},
|
||
},
|
||
update: {
|
||
data: input.data,
|
||
totalProducts: input.totalProducts,
|
||
totalStocks: input.totalStocks,
|
||
totalReserved: input.totalReserved,
|
||
},
|
||
create: {
|
||
organizationId: user.organization.id,
|
||
cacheDate: today,
|
||
data: input.data,
|
||
totalProducts: input.totalProducts,
|
||
totalStocks: input.totalStocks,
|
||
totalReserved: input.totalReserved,
|
||
},
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
message: "Кеш склада WB успешно сохранен",
|
||
cache: {
|
||
...cache,
|
||
cacheDate: cache.cacheDate.toISOString().split("T")[0],
|
||
createdAt: cache.createdAt.toISOString(),
|
||
updatedAt: cache.updatedAt.toISOString(),
|
||
},
|
||
fromCache: false,
|
||
};
|
||
} catch (error) {
|
||
console.error("Error saving WB warehouse cache:", error);
|
||
return {
|
||
success: false,
|
||
message:
|
||
error instanceof Error
|
||
? error.message
|
||
: "Ошибка сохранения кеша склада WB",
|
||
cache: null,
|
||
fromCache: false,
|
||
};
|
||
}
|
||
},
|
||
};
|
||
|
||
// Добавляем админ запросы и мутации к основным резолверам
|
||
resolvers.Query = {
|
||
...resolvers.Query,
|
||
...adminQueries,
|
||
...wildberriesQueries,
|
||
...externalAdQueries,
|
||
...wbWarehouseCacheQueries,
|
||
};
|
||
|
||
resolvers.Mutation = {
|
||
...resolvers.Mutation,
|
||
...adminMutations,
|
||
...externalAdMutations,
|
||
...wbWarehouseCacheMutations,
|
||
};
|