feat: завершить миграцию на доменно-модульную архитектуру резолверов

🏗️ КРУПНОЕ РЕФАКТОРИНГ: Полный переход от монолитной к доменной архитектуре

 УДАЛЕНЫ V2 файлы (8 шт):
- employees-v2.ts, fulfillment-*-v2.ts, goods-supply-v2.ts
- logistics-consumables-v2.ts, seller-inventory-v2.ts
- Функционал перенесен в соответствующие domains/

 УДАЛЕНЫ пустые заглушки (2 шт):
- employees.ts, supplies.ts (содержали только пустые объекты)

 УДАЛЕНЫ дубликаты (3 шт):
- logistics.ts, referrals.ts, seller-consumables.ts
- Заменены версиями из domains/

 АРХИВИРОВАН старый монолит:
- src/graphql/resolvers.ts (354KB) → temp/archive/
- Не использовался, имел сломанные V2 импорты

🔄 РЕОРГАНИЗАЦИЯ:
- auth.ts перемещен в domains/auth.ts
- Обновлены импорты в resolvers/index.ts
- Удалены закомментированные V2 импорты

🚀 ДОБАВЛЕНА недостающая функция:
- fulfillmentReceiveConsumableSupply в domains/inventory.ts
- Полная поддержка приемки товаров фулфилментом

📊 РЕЗУЛЬТАТ:
- Чистая доменная архитектура без legacy кода
- Все функции V1→V2 миграции сохранены
- Система полностью готова к дальнейшему развитию

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-09-12 17:07:32 +03:00
parent 8d4b9ce97f
commit 2269de6c85
22 changed files with 3858 additions and 15718 deletions

View File

@ -1,263 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Варианты кнопки "Назад"</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.sidebar-mock {
width: 240px;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.block-mock {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 16px;
height: 180px;
}
</style>
</head>
<body class="p-8 text-white">
<h1 class="text-3xl font-bold mb-8 text-center">Варианты размещения кнопки "Назад"</h1>
<!-- ВАРИАНТ 1: ПЛАВАЮЩАЯ КНОПКА СЛЕВА -->
<div class="mb-12">
<h2 class="text-xl font-semibold mb-4">🌟 Вариант 1: Плавающая кнопка слева</h2>
<div class="flex gap-4 h-48">
<!-- Sidebar Mock -->
<div class="sidebar-mock rounded-2xl p-4 flex flex-col">
<div class="flex items-center gap-3 mb-6">
<div class="w-8 h-8 bg-purple-500 rounded-full"></div>
<span class="text-sm">Rennel</span>
</div>
<div class="space-y-2">
<div class="text-sm opacity-60">Главная</div>
<div class="text-sm opacity-60">Маркет</div>
<div class="text-sm opacity-60">Мессенджер</div>
</div>
</div>
<!-- Floating Back Button -->
<div class="relative">
<button class="absolute left-0 top-6 z-10 w-8 h-8 glass rounded-full flex items-center justify-center transition-all duration-300 hover:scale-110 hover:bg-white/20 group">
<svg class="h-4 w-4 text-white/70 group-hover:text-white group-hover:h-5 group-hover:w-5 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
</div>
<!-- Block 1 Mock -->
<div class="flex-1 block-mock p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<svg class="h-5 w-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
<h2 class="text-lg font-semibold">Поставщики</h2>
</div>
<div class="w-64 glass rounded-full px-3 py-1.5 text-sm">
<span class="text-white/60">Поиск поставщиков...</span>
</div>
</div>
</div>
</div>
</div>
<!-- ВАРИАНТ 2: ПОЛОСА НАВИГАЦИИ СВЕРХУ -->
<div class="mb-12">
<h2 class="text-xl font-semibold mb-4">📍 Вариант 2: Полоса навигации сверху</h2>
<div class="flex gap-4">
<!-- Sidebar Mock -->
<div class="sidebar-mock rounded-2xl p-4 flex flex-col h-48">
<div class="flex items-center gap-3 mb-6">
<div class="w-8 h-8 bg-purple-500 rounded-full"></div>
<span class="text-sm">Rennel</span>
</div>
<div class="space-y-2">
<div class="text-sm opacity-60">Главная</div>
<div class="text-sm opacity-60">Маркет</div>
<div class="text-sm opacity-60">Мессенджер</div>
</div>
</div>
<!-- Content Area -->
<div class="flex-1">
<!-- Navigation Bar -->
<div class="flex items-center justify-start mb-4">
<button class="w-6 h-6 hover:w-8 hover:h-8 bg-gradient-to-r from-purple-500/20 to-blue-500/20 hover:from-purple-500/40 hover:to-blue-500/40 rounded-full flex items-center justify-center transition-all duration-300 backdrop-blur-sm border border-white/10 hover:border-white/30 group">
<svg class="h-3 w-3 group-hover:h-4 group-hover:w-4 text-white/60 group-hover:text-white transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
</div>
<!-- Block 1 Mock -->
<div class="block-mock p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<svg class="h-5 w-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
<h2 class="text-lg font-semibold">Поставщики</h2>
</div>
<div class="w-64 glass rounded-full px-3 py-1.5 text-sm">
<span class="text-white/60">Поиск поставщиков...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ВАРИАНТ 3: КНОПКА В РАЗРЫВЕ -->
<div class="mb-12">
<h2 class="text-xl font-semibold mb-4">🎯 Вариант 3: Кнопка в разрыве</h2>
<div class="flex gap-4 h-48 relative">
<!-- Sidebar Mock -->
<div class="sidebar-mock rounded-2xl p-4 flex flex-col">
<div class="flex items-center gap-3 mb-6">
<div class="w-8 h-8 bg-purple-500 rounded-full"></div>
<span class="text-sm">Rennel</span>
</div>
<div class="space-y-2">
<div class="text-sm opacity-60">Главная</div>
<div class="text-sm opacity-60">Маркет</div>
<div class="text-sm opacity-60">Мессенджер</div>
</div>
</div>
<!-- Button in Gap -->
<div class="w-0 flex items-center justify-center relative">
<button class="absolute w-10 h-10 glass rounded-xl hover:bg-white/15 flex items-center justify-center transition-all duration-500 hover:scale-125 hover:rotate-12 hover:border-purple-400/50 group backdrop-blur-md">
<svg class="h-4 w-4 text-white/50 group-hover:text-purple-300 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
</div>
<!-- Block 1 Mock -->
<div class="flex-1 block-mock p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<svg class="h-5 w-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
<h2 class="text-lg font-semibold">Поставщики</h2>
</div>
<div class="w-64 glass rounded-full px-3 py-1.5 text-sm">
<span class="text-white/60">Поиск поставщиков...</span>
</div>
</div>
</div>
</div>
</div>
<!-- ВАРИАНТ 4: BREADCRUMB СТИЛЬ -->
<div class="mb-12">
<h2 class="text-xl font-semibold mb-4">🍞 Вариант 4: Breadcrumb стиль</h2>
<div class="flex gap-4">
<!-- Sidebar Mock -->
<div class="sidebar-mock rounded-2xl p-4 flex flex-col h-48">
<div class="flex items-center gap-3 mb-6">
<div class="w-8 h-8 bg-purple-500 rounded-full"></div>
<span class="text-sm">Rennel</span>
</div>
<div class="space-y-2">
<div class="text-sm opacity-60">Главная</div>
<div class="text-sm opacity-60">Маркет</div>
<div class="text-sm opacity-60">Мессенджер</div>
</div>
</div>
<!-- Content Area -->
<div class="flex-1">
<!-- Breadcrumb -->
<div class="flex items-center gap-2 mb-2 pl-2">
<button class="p-1 hover:p-2 bg-white/0 hover:bg-white/10 rounded-lg transition-all duration-300 group">
<svg class="h-3 w-3 group-hover:h-4 group-hover:w-4 text-white/40 group-hover:text-white transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<span class="text-white/20 text-sm"></span>
<span class="text-white/60 text-sm">Поставщики</span>
</div>
<!-- Block 1 Mock -->
<div class="block-mock p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<svg class="h-5 w-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
<h2 class="text-lg font-semibold">Поставщики</h2>
</div>
<div class="w-64 glass rounded-full px-3 py-1.5 text-sm">
<span class="text-white/60">Поиск поставщиков...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ВАРИАНТ 5: ВЕРТИКАЛЬНАЯ ПАНЕЛЬ -->
<div class="mb-12">
<h2 class="text-xl font-semibold mb-4">🔥 Вариант 5: Вертикальная панель</h2>
<div class="flex gap-4 h-48">
<!-- Sidebar Mock -->
<div class="sidebar-mock rounded-2xl p-4 flex flex-col">
<div class="flex items-center gap-3 mb-6">
<div class="w-8 h-8 bg-purple-500 rounded-full"></div>
<span class="text-sm">Rennel</span>
</div>
<div class="space-y-2">
<div class="text-sm opacity-60">Главная</div>
<div class="text-sm opacity-60">Маркет</div>
<div class="text-sm opacity-60">Мессенджер</div>
</div>
</div>
<!-- Vertical Panel -->
<div class="w-8 flex flex-col items-center py-4">
<button class="w-6 h-6 hover:w-7 hover:h-7 bg-gradient-to-b from-purple-500/10 to-transparent hover:from-purple-500/30 hover:to-purple-400/10 rounded-full flex items-center justify-center transition-all duration-300 hover:shadow-lg hover:shadow-purple-500/20 group">
<svg class="h-3 w-3 group-hover:h-4 group-hover:w-4 text-white/50 group-hover:text-purple-300 transition-all duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
</div>
<!-- Block 1 Mock -->
<div class="flex-1 block-mock p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<svg class="h-5 w-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
<h2 class="text-lg font-semibold">Поставщики</h2>
</div>
<div class="w-64 glass rounded-full px-3 py-1.5 text-sm">
<span class="text-white/60">Поиск поставщиков...</span>
</div>
</div>
</div>
</div>
</div>
<div class="text-center text-white/60 mt-8">
<p>Наведите курсор на кнопки для просмотра hover эффектов</p>
</div>
</body>
</html>

View File

@ -1,96 +0,0 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
async function diagnoseDatabase() {
try {
console.log("🔍 ДИАГНОСТИКА БАЗЫ ДАННЫХ...\n");
// Проверяем пользователей
const users = await prisma.user.findMany({
include: {
organization: true,
},
});
console.log("👥 ПОЛЬЗОВАТЕЛИ:");
users.forEach((user) => {
console.log(` - ID: ${user.id}`);
console.log(` Телефон: ${user.phone}`);
console.log(` Организация: ${user.organization?.name || "НЕТ"}`);
console.log(` Тип организации: ${user.organization?.type || "НЕТ"}`);
console.log("");
});
// Проверяем организации
const organizations = await prisma.organization.findMany();
console.log("🏢 ОРГАНИЗАЦИИ:");
organizations.forEach((org) => {
console.log(` - ID: ${org.id}`);
console.log(` Название: ${org.name}`);
console.log(` Тип: ${org.type}`);
console.log("");
});
// Проверяем товары
const products = await prisma.product.findMany({
include: {
organization: true,
category: true,
},
orderBy: {
createdAt: "desc",
},
});
console.log("🛍️ ТОВАРЫ:");
if (products.length === 0) {
console.log(" НЕТ ТОВАРОВ В БАЗЕ ДАННЫХ");
} else {
products.forEach((product) => {
console.log(` - ID: ${product.id}`);
console.log(` Название: ${product.name}`);
console.log(` Артикул: ${product.article}`);
console.log(` Тип: ${product.type}`);
console.log(` Активен: ${product.isActive}`);
console.log(
` Организация: ${product.organization?.name || "НЕТ"} (${
product.organization?.type || "НЕТ"
})`
);
console.log(` Создан: ${product.createdAt}`);
console.log("");
});
}
// Проверяем товары поставщиков
const wholesaleProducts = await prisma.product.findMany({
where: {
organization: {
type: "WHOLESALE",
},
type: "PRODUCT",
},
include: {
organization: true,
},
});
console.log("🏪 ТОВАРЫ ПОСТАВЩИКОВ (WHOLESALE + PRODUCT):");
if (wholesaleProducts.length === 0) {
console.log(" НЕТ ТОВАРОВ ПОСТАВЩИКОВ");
} else {
wholesaleProducts.forEach((product) => {
console.log(
` - ${product.name} (${product.article}) - ${product.organization?.name}`
);
});
}
} catch (error) {
console.error("❌ ОШИБКА:", error);
} finally {
await prisma.$disconnect();
}
}
diagnoseDatabase();

View File

@ -1,151 +0,0 @@
-- =============================================================================
-- 📦 МИГРАЦИЯ ДЛЯ СИСТЕМЫ ПОСТАВОК РАСХОДНИКОВ СЕЛЛЕРА
-- =============================================================================
-- Создание: Новые таблицы для селлерских поставок расходников
-- Автор: Claude Code AI Assistant
-- Дата: $(date)
-- Создание нового enum для статусов селлера (5-статусная система)
CREATE TYPE "SellerSupplyOrderStatus" AS ENUM (
'PENDING', -- Ожидает одобрения поставщика
'APPROVED', -- Одобрено поставщиком
'SHIPPED', -- Отгружено
'DELIVERED', -- Доставлено
'COMPLETED', -- Завершено
'CANCELLED' -- Отменено
);
-- Основная таблица поставок расходников селлера
CREATE TABLE "seller_consumable_supply_orders" (
-- === БАЗОВЫЕ ПОЛЯ ===
"id" TEXT NOT NULL,
"status" "SellerSupplyOrderStatus" NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
-- === ДАННЫЕ СЕЛЛЕРА (создатель) ===
"sellerId" TEXT NOT NULL, -- кто заказывает (FK: Organization SELLER)
"fulfillmentCenterId" TEXT NOT NULL, -- куда доставлять (FK: Organization FULFILLMENT)
"requestedDeliveryDate" TIMESTAMP(3) NOT NULL, -- когда нужно
"notes" TEXT, -- заметки селлера
-- === ДАННЫЕ ПОСТАВЩИКА ===
"supplierId" TEXT, -- кто поставляет (FK: Organization WHOLESALE)
"supplierApprovedAt" TIMESTAMP(3), -- когда одобрил
"packagesCount" INTEGER, -- количество грузомест
"estimatedVolume" DECIMAL(8,3), -- объем груза в м³
"supplierContractId" TEXT, -- номер договора
"supplierNotes" TEXT, -- заметки поставщика
-- === ДАННЫЕ ЛОГИСТИКИ ===
"logisticsPartnerId" TEXT, -- кто везет (FK: Organization LOGIST)
"estimatedDeliveryDate" TIMESTAMP(3), -- план доставки
"routeId" TEXT, -- маршрут (FK: LogisticsRoute)
"logisticsCost" DECIMAL(10,2), -- стоимость доставки
"logisticsNotes" TEXT, -- заметки логистики
-- === ДАННЫЕ ОТГРУЗКИ ===
"shippedAt" TIMESTAMP(3), -- факт отгрузки
"trackingNumber" TEXT, -- номер отслеживания
-- === ДАННЫЕ ПРИЕМКИ ===
"deliveredAt" TIMESTAMP(3), -- факт доставки в ФФ
"receivedById" TEXT, -- кто принял в ФФ (FK: User)
"actualQuantity" INTEGER, -- принято количество
"defectQuantity" INTEGER, -- брак
"receiptNotes" TEXT, -- заметки приемки
-- === ЭКОНОМИКА (для будущего раздела экономики) ===
"totalCostWithDelivery" DECIMAL(12,2), -- общая стоимость с доставкой
"estimatedStorageCost" DECIMAL(10,2), -- оценочная стоимость хранения
CONSTRAINT "seller_consumable_supply_orders_pkey" PRIMARY KEY ("id")
);
-- Позиции в поставке расходников селлера
CREATE TABLE "seller_consumable_supply_items" (
"id" TEXT NOT NULL,
"supplyOrderId" TEXT NOT NULL, -- связь с поставкой
"productId" TEXT NOT NULL, -- какой расходник (FK: Product)
-- === КОЛИЧЕСТВА ===
"requestedQuantity" INTEGER NOT NULL, -- запросили
"approvedQuantity" INTEGER, -- поставщик одобрил
"shippedQuantity" INTEGER, -- отгрузили
"receivedQuantity" INTEGER, -- приняли в ФФ
"defectQuantity" INTEGER DEFAULT 0, -- брак
-- === ЦЕНЫ ===
"unitPrice" DECIMAL(10,2) NOT NULL, -- цена за единицу от поставщика
"totalPrice" DECIMAL(12,2) NOT NULL, -- общая стоимость
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "seller_consumable_supply_items_pkey" PRIMARY KEY ("id")
);
-- === СОЗДАНИЕ ИНДЕКСОВ ===
CREATE UNIQUE INDEX "seller_consumable_supply_items_supplyOrderId_productId_key"
ON "seller_consumable_supply_items"("supplyOrderId", "productId");
-- === СОЗДАНИЕ ВНЕШНИХ КЛЮЧЕЙ ===
-- Seller Supply Orders связи
ALTER TABLE "seller_consumable_supply_orders"
ADD CONSTRAINT "seller_consumable_supply_orders_sellerId_fkey"
FOREIGN KEY ("sellerId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "seller_consumable_supply_orders"
ADD CONSTRAINT "seller_consumable_supply_orders_fulfillmentCenterId_fkey"
FOREIGN KEY ("fulfillmentCenterId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "seller_consumable_supply_orders"
ADD CONSTRAINT "seller_consumable_supply_orders_supplierId_fkey"
FOREIGN KEY ("supplierId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "seller_consumable_supply_orders"
ADD CONSTRAINT "seller_consumable_supply_orders_logisticsPartnerId_fkey"
FOREIGN KEY ("logisticsPartnerId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "seller_consumable_supply_orders"
ADD CONSTRAINT "seller_consumable_supply_orders_receivedById_fkey"
FOREIGN KEY ("receivedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- Seller Supply Items связи
ALTER TABLE "seller_consumable_supply_items"
ADD CONSTRAINT "seller_consumable_supply_items_supplyOrderId_fkey"
FOREIGN KEY ("supplyOrderId") REFERENCES "seller_consumable_supply_orders"("id") ON DELETE CASCADE;
ALTER TABLE "seller_consumable_supply_items"
ADD CONSTRAINT "seller_consumable_supply_items_productId_fkey"
FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- === ДОБАВЛЕНИЕ СВЯЗЕЙ В СУЩЕСТВУЮЩИЕ ТАБЛИЦЫ ===
-- Добавление связей в organizations (если они еще не существуют)
DO $$
BEGIN
-- Проверяем существование колонок перед добавлением
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'organizations'
AND column_name = 'sellerSupplyOrdersAsSeller'
) THEN
-- Добавляем связи будут созданы автоматически через Prisma при следующем generate
RAISE NOTICE 'Связи для селлерских поставок будут созданы автоматически при prisma generate';
END IF;
END
$$;
-- Комментарии к таблицам
COMMENT ON TABLE "seller_consumable_supply_orders" IS 'Поставки расходников селлера - заказы от селлеров для доставки в фулфилмент-центры';
COMMENT ON TABLE "seller_consumable_supply_items" IS 'Позиции в поставках расходников селлера';
-- Комментарии к ключевым полям
COMMENT ON COLUMN "seller_consumable_supply_orders"."sellerId" IS 'Селлер-заказчик (тип SELLER)';
COMMENT ON COLUMN "seller_consumable_supply_orders"."fulfillmentCenterId" IS 'Фулфилмент-получатель (тип FULFILLMENT)';
COMMENT ON COLUMN "seller_consumable_supply_orders"."supplierId" IS 'Поставщик товаров (тип WHOLESALE)';
COMMENT ON COLUMN "seller_consumable_supply_orders"."totalCostWithDelivery" IS 'Для будущего раздела экономики селлера';
RAISE NOTICE 'Система поставок расходников селлера успешно создана!';

3732
server.log
View File

@ -2,16 +2,3734 @@
> sferav@0.1.0 dev
> next dev --turbopack
⚠ Port 3000 is in use by process 17170
18649
23448
33312, using available port 3001 instead.
▲ Next.js 15.4.1 (Turbopack)
- Local: http://localhost:3001
- Network: http://192.168.0.101:3001
- Local: http://localhost:3000
- Network: http://192.168.50.224:3000
- Environments: .env
- Experiments (use with caution):
· optimizePackageImports
✓ Starting...
✓ Ready in 897ms
✓ Ready in 840ms
○ Compiling /api/graphql ...
✓ Compiled /api/graphql in 1224ms
🔥 МОДУЛЬ SERVICES DOMAIN ЗАГРУЖАЕТСЯ
🔥 SERVICES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 INVENTORY DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER GOODS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 LOGISTICS CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 WILDBERRIES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ANALYTICS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ADMIN TOOLS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 FILE MANAGEMENT DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 EXTERNAL ADS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
GraphQL Context - Invalid token: Error [JsonWebTokenError]: jwt malformed
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
constructor: [Function: JsonWebTokenError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: undefined,
timestamp: '2025-09-10T15:44:17.607Z',
variables: undefined
}
🔐 EMPLOYEE DOMAIN AUTH CHECK: { hasUser: false, userId: undefined, organizationId: undefined }
❌ AUTH FAILED: No user in context
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: undefined,
timestamp: '2025-09-10T15:44:17.610Z'
}
POST /api/graphql 200 in 1578ms
GraphQL Context - Invalid token: Error [JsonWebTokenError]: jwt malformed
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
constructor: [Function: JsonWebTokenError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: undefined,
timestamp: '2025-09-10T15:45:07.294Z',
variables: undefined
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: undefined,
timestamp: '2025-09-10T15:45:07.295Z'
}
POST /api/graphql 200 in 56ms
GraphQL Context - Invalid token: Error [JsonWebTokenError]: jwt malformed
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
constructor: [Function: JsonWebTokenError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: undefined,
timestamp: '2025-09-10T15:45:29.404Z',
variables: undefined
}
🔐 SELLER GOODS DOMAIN AUTH CHECK: { hasUser: false, userId: undefined, organizationId: undefined }
❌ AUTH FAILED: No user in context
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: undefined,
timestamp: '2025-09-10T15:45:29.405Z'
}
POST /api/graphql 200 in 37ms
○ Compiling /fulfillment/supplies/goods/new ...
✓ Compiled /fulfillment/supplies/goods/new in 1780ms
GET /fulfillment/supplies/goods/new 200 in 2042ms
✓ Compiled /favicon.ico in 134ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 395ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:05.568Z',
variables: {}
}
POST /api/graphql 200 in 1411ms
✓ Compiled /api/events in 104ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:06.265Z',
variables: {}
}
POST /api/graphql 200 in 483ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:06.739Z',
variables: {}
}
POST /api/graphql 200 in 458ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T15:47:07.015Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T15:47:07.026Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T15:47:07.036Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMySellerGoodsSupplyRequests',
timestamp: '2025-09-10T15:47:07.219Z',
variables: {}
}
🔐 SELLER GOODS DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfbgh2wl0001y5nap24fasui',
organizationId: 'cmfbghrno0002y5na8b59ykfr'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_SELLER_GOODS_SUPPLY_REQUESTS DOMAIN QUERY STARTED: { userId: 'cmfbgh2wl0001y5nap24fasui' }
❌ MY_SELLER_GOODS_SUPPLY_REQUESTS DOMAIN ERROR: GraphQLError: Доступно только для поставщиков
at checkWholesaleAccess (src/graphql/resolvers/domains/seller-goods.ts:95:10)
at async (src/graphql/resolvers/domains/seller-goods.ts:183:21)
at async Object.mySellerGoodsSupplyRequests (src/graphql/resolvers/domains/seller-goods.ts:30:21)
93 |
94 | if (!user.organization || user.organization.type !== 'WHOLESALE') {
> 95 | throw new GraphQLError('Доступно только для поставщиков', {
| ^
96 | extensions: { code: 'FORBIDDEN' },
97 | })
98 | } {
path: undefined,
locations: undefined,
extensions: [Object]
}
🎯 RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 396ms
📥 INCOMING_REQUESTS: {
userId: 'cmfbgh2wl0001y5nap24fasui',
organizationId: 'cmfbghrno0002y5na8b59ykfr',
requestsCount: 0
}
POST /api/graphql 200 in 1417ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyEmployeesV2',
timestamp: '2025-09-10T15:47:07.606Z',
variables: {}
}
🔐 EMPLOYEE DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfbgh2wl0001y5nap24fasui',
organizationId: 'cmfbghrno0002y5na8b59ykfr'
}
✅ AUTH PASSED: Calling resolver
🔐 EMPLOYEE DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfbgh2wl0001y5nap24fasui',
organizationId: 'cmfbghrno0002y5na8b59ykfr'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_EMPLOYEES DOMAIN QUERY STARTED: { args: {}, userId: 'cmfbgh2wl0001y5nap24fasui' }
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetLogisticsPartners',
timestamp: '2025-09-10T15:47:07.636Z',
variables: {}
}
📦 LOGISTICS_PARTNERS RESOLVER CALLED: {
organizationId: 'cmfbghrno0002y5na8b59ykfr',
organizationType: 'FULFILLMENT',
timestamp: '2025-09-10T15:47:07.799Z'
}
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfbgh2wl0001y5nap24fasui',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
❌ GraphQL ERROR: {
errors: [
'Cannot return null for non-nullable field PendingSuppliesCount.incomingRequests.'
],
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T15:47:07.910Z'
}
POST /api/graphql 200 in 1888ms
📊 LOGISTICS_PARTNERS RESULT: { partnersCount: 0, organizationType: 'FULFILLMENT' }
POST /api/graphql 200 in 517ms
POST /api/graphql 200 in 1965ms
✅ MY_EMPLOYEES DOMAIN SUCCESS: { total: 0, page: 1, employeesCount: 0 }
🎯 RESOLVER RESULT TYPE: object Has result
🎯 RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 698ms
✓ Compiled /fulfillment/supplies/fulfillment-consumables in 340ms
GET /fulfillment/supplies/fulfillment-consumables 200 in 372ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZiZ2gyd2wwMDAxeTVuYXAyNGZhc3VpIiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NzQwNzQzMCwiZXhwIjoxNzU5OTk5NDMwfQ.jw0t2qqwtuqBPbzJZ71iLim623iK4y8XCRtbByg8-Lw&orgId=cmfbghrno0002y5na8b59ykfr 200 in 2889ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:11.005Z',
variables: {}
}
POST /api/graphql 200 in 518ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyFulfillmentConsumableSupplies',
timestamp: '2025-09-10T15:47:11.562Z',
variables: {}
}
🔐 INVENTORY DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfbgh2wl0001y5nap24fasui',
organizationId: 'cmfbghrno0002y5na8b59ykfr'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_FULFILLMENT_CONSUMABLE_SUPPLIES DOMAIN QUERY STARTED: { userId: 'cmfbgh2wl0001y5nap24fasui' }
✅ MY_FULFILLMENT_CONSUMABLE_SUPPLIES DOMAIN SUCCESS: { count: 0 }
🎯 RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 549ms
✓ Compiled /supplies/create-fulfillment-consumables-v2 in 356ms
GET /supplies/create-fulfillment-consumables-v2 200 in 386ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZiZ2gyd2wwMDAxeTVuYXAyNGZhc3VpIiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NzQwNzQzMCwiZXhwIjoxNzU5OTk5NDMwfQ.jw0t2qqwtuqBPbzJZ71iLim623iK4y8XCRtbByg8-Lw&orgId=cmfbghrno0002y5na8b59ykfr 200 in 7485ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:13.729Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyCounterparties',
timestamp: '2025-09-10T15:47:13.733Z',
variables: {}
}
POST /api/graphql 200 in 441ms
🤝 MY_COUNTERPARTIES: {
userId: 'cmfbgh2wl0001y5nap24fasui',
organizationId: 'cmfbghrno0002y5na8b59ykfr',
organizationType: 'FULFILLMENT',
counterpartiesCount: 1
}
POST /api/graphql 200 in 857ms
✓ Compiled /fulfillment/partners in 445ms
GET /fulfillment/partners 200 in 473ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:23.909Z',
variables: {}
}
POST /api/graphql 200 in 532ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetOutgoingRequests',
timestamp: '2025-09-10T15:47:24.469Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyPartnerLink',
timestamp: '2025-09-10T15:47:24.553Z',
variables: {}
}
🔐 REFERRALS DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfbgh2wl0001y5nap24fasui',
organizationId: 'cmfbghrno0002y5na8b59ykfr'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_PARTNER_LINK DOMAIN QUERY STARTED: { userId: 'cmfbgh2wl0001y5nap24fasui' }
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:24.559Z',
variables: {}
}
📤 OUTGOING_REQUESTS: {
userId: 'cmfbgh2wl0001y5nap24fasui',
organizationId: 'cmfbghrno0002y5na8b59ykfr',
requestsCount: 0
}
POST /api/graphql 200 in 561ms
POST /api/graphql 200 in 564ms
✅ MY_PARTNER_LINK DOMAIN SUCCESS: { link: 'http://localhost:3000/register?partner=AF5DFT94SX' }
🎯 RESOLVER RESULT TYPE: string Has result
POST /api/graphql 200 in 643ms
✓ Compiled /register in 240ms
🔍 RegisterContent - URL параметры: {
partnerCode: 'AF5DFT94SX',
referralCode: null,
searchParams: { partner: 'AF5DFT94SX' }
}
🚀 RegisterContent - Передача в AuthFlow: { partnerCode: 'AF5DFT94SX', referralCode: null }
🎯 RegisterContent - Принудительный показ AuthFlow из-за наличия кода
🎢 AuthFlow - Полученные props: { partnerCode: 'AF5DFT94SX', referralCode: null }
🎢 AuthFlow - Статус авторизации: { isAuthenticated: false, hasUser: false }
🎢 AuthFlow - Обработанные данные: { registrationType: 'PARTNER', activeCode: 'AF5DFT94SX' }
🎢 AuthFlow - Сохраненные в authData: { partnerCode: 'AF5DFT94SX', referralCode: null }
GET /register?partner=AF5DFT94SX 200 in 320ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:38.498Z',
variables: {}
}
POST /api/graphql 200 in 466ms
✓ Compiled /dashboard in 284ms
GET /dashboard 200 in 347ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:41.542Z',
variables: {}
}
POST /api/graphql 200 in 538ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T15:47:42.067Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T15:47:42.158Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:42.162Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T15:47:42.172Z',
variables: {}
}
POST /api/graphql 200 in 570ms
📥 INCOMING_REQUESTS: {
userId: 'cmfbgm9c90004y5naqzw76bxd',
organizationId: 'cmfbgmqer0005y5na1ezlc4aw',
requestsCount: 0
}
POST /api/graphql 200 in 656ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfbgm9c90004y5naqzw76bxd',
organizationType: 'SELLER',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
❌ GraphQL ERROR: {
errors: [
'Cannot return null for non-nullable field PendingSuppliesCount.incomingRequests.'
],
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T15:47:42.883Z'
}
POST /api/graphql 200 in 1045ms
POST /api/graphql 200 in 1218ms
✓ Compiled /seller/partners in 327ms
GET /seller/partners 200 in 357ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:45.297Z',
variables: {}
}
POST /api/graphql 200 in 456ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:45.834Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetOutgoingRequests',
timestamp: '2025-09-10T15:47:45.840Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyPartnerLink',
timestamp: '2025-09-10T15:47:45.851Z',
variables: {}
}
🔐 REFERRALS DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfbgm9c90004y5naqzw76bxd',
organizationId: 'cmfbgmqer0005y5na1ezlc4aw'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_PARTNER_LINK DOMAIN QUERY STARTED: { userId: 'cmfbgm9c90004y5naqzw76bxd' }
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyCounterparties',
timestamp: '2025-09-10T15:47:45.855Z',
variables: {}
}
POST /api/graphql 200 in 463ms
📤 OUTGOING_REQUESTS: {
userId: 'cmfbgm9c90004y5naqzw76bxd',
organizationId: 'cmfbgmqer0005y5na1ezlc4aw',
requestsCount: 0
}
POST /api/graphql 200 in 545ms
✅ MY_PARTNER_LINK DOMAIN SUCCESS: { link: 'http://localhost:3000/register?partner=BECP6AJGWK' }
🎯 RESOLVER RESULT TYPE: string Has result
POST /api/graphql 200 in 557ms
🤝 MY_COUNTERPARTIES: {
userId: 'cmfbgm9c90004y5naqzw76bxd',
organizationId: 'cmfbgmqer0005y5na1ezlc4aw',
organizationType: 'SELLER',
counterpartiesCount: 1
}
POST /api/graphql 200 in 965ms
○ Compiling /wholesale/orders ...
✓ Compiled /wholesale/orders in 740ms
GET /wholesale/orders 200 in 854ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:58.035Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:58.036Z'
}
POST /api/graphql 200 in 228ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:58.206Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T15:47:58.207Z'
}
POST /api/graphql 200 in 115ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendSmsCode',
timestamp: '2025-09-10T15:48:03.541Z',
variables: { phone: '77777777777' }
}
POST /api/graphql 200 in 143ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:48:03.687Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T15:48:03.688Z'
}
POST /api/graphql 200 in 121ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifySmsCode',
timestamp: '2025-09-10T15:48:06.048Z',
variables: { phone: '77777777777', code: '1234' }
}
POST /api/graphql 200 in 812ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T15:48:30.227Z',
variables: { inn: '7743291031' }
}
POST /api/graphql 200 in 249ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T15:48:58.778Z',
variables: { inn: '7743291031' }
}
POST /api/graphql 200 in 219ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T15:49:29.720Z',
variables: { inn: '7736207543' }
}
POST /api/graphql 200 in 228ms
🔥 МОДУЛЬ SERVICES DOMAIN ЗАГРУЖАЕТСЯ
🔥 SERVICES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 INVENTORY DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER GOODS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 LOGISTICS CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 WILDBERRIES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ANALYTICS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ADMIN TOOLS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 FILE MANAGEMENT DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 EXTERNAL ADS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: undefined,
timestamp: '2025-09-10T15:52:45.912Z',
variables: undefined
}
🔍 VERIFY_INN STARTED: { inn: '7743291031' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '7743291031', name: 'А-Я ЛОГИСТИКА', isActive: true }
POST /api/graphql 200 in 752ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: undefined,
timestamp: '2025-09-10T15:53:02.793Z',
variables: undefined
}
🔍 VERIFY_INN STARTED: { inn: '7736207543' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '7736207543', name: 'ЯНДЕКС', isActive: true }
POST /api/graphql 200 in 334ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: undefined,
timestamp: '2025-09-10T15:53:12.807Z',
variables: undefined
}
🔍 VERIFY_INN STARTED: { inn: '1234567890' }
❌ VERIFY_INN: ИНН не прошел валидацию контрольной суммы: 1234567890
POST /api/graphql 200 in 48ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: undefined,
timestamp: '2025-09-10T15:53:24.872Z',
variables: undefined
}
🔍 VERIFY_INN STARTED: { inn: '7702070139' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '7702070139', name: 'БАНК ВТБ', isActive: true }
POST /api/graphql 200 in 321ms
🔥 МОДУЛЬ SERVICES DOMAIN ЗАГРУЖАЕТСЯ
🔥 SERVICES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 INVENTORY DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER GOODS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 LOGISTICS CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 WILDBERRIES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ANALYTICS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ADMIN TOOLS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 FILE MANAGEMENT DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 EXTERNAL ADS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: undefined,
timestamp: '2025-09-10T15:54:37.860Z',
variables: undefined
}
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfbgh2wl0001y5nap24fasui',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 2138ms
GET /wholesale/orders 200 in 114ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:54:49.610Z',
variables: {}
}
POST /api/graphql 200 in 219ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 254ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:54:49.873Z',
variables: {}
}
POST /api/graphql 200 in 217ms
✓ Compiled /login in 298ms
GET /login 200 in 345ms
GET /login 200 in 31ms
GET /dashboard 200 in 29ms
GET /dashboard 200 in 33ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:54:50.303Z',
variables: {}
}
POST /api/graphql 200 in 197ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:54:50.525Z',
variables: {}
}
POST /api/graphql 200 in 208ms
GET /dashboard 200 in 66ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 227ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendSmsCode',
timestamp: '2025-09-10T15:54:53.797Z',
variables: { phone: '77777777777' }
}
POST /api/graphql 200 in 74ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifySmsCode',
timestamp: '2025-09-10T15:54:55.838Z',
variables: { phone: '77777777777', code: '1234' }
}
POST /api/graphql 200 in 305ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T15:54:59.690Z',
variables: { inn: '7743291031' }
}
🔍 VERIFY_INN STARTED: { inn: '7743291031' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '7743291031', name: 'А-Я ЛОГИСТИКА', isActive: true }
POST /api/graphql 200 in 454ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:55:01.663Z',
variables: {}
}
POST /api/graphql 200 in 222ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'RegisterFulfillmentOrganization',
timestamp: '2025-09-10T15:55:02.759Z',
variables: {
input: {
phone: '77777777777',
inn: '7743291031',
type: 'LOGIST',
referralCode: null,
partnerCode: null
}
}
}
🏢 REGISTER_FULFILLMENT_ORGANIZATION - ВЫЗВАН: {
phone: '77777777777',
inn: '7743291031',
referralCode: null,
timestamp: '2025-09-10T15:55:02.760Z'
}
✅ ФУЛФИЛМЕНТ ОРГАНИЗАЦИЯ СОЗДАНА: {
organizationId: 'cmfe5upy30001y56e4av0o4vs',
userId: 'cmfe5lscj0000y56eeg95r8lr',
inn: '7743291031',
type: 'FULFILLMENT',
referralCode: 'FF_7743291031_1757519703002'
}
POST /api/graphql 200 in 1640ms
GET /dashboard 200 in 59ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:55:06.669Z',
variables: {}
}
POST /api/graphql 200 in 443ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T15:55:07.190Z',
variables: {}
}
📥 INCOMING_REQUESTS: {
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationId: 'cmfe5upy30001y56e4av0o4vs',
requestsCount: 0
}
POST /api/graphql 200 in 563ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T15:55:07.948Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T15:55:07.968Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T15:55:07.970Z',
variables: {}
}
POST /api/graphql 200 in 1427ms
POST /api/graphql 200 in 1430ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1892ms
GET /fulfillment/supplies/goods/new 200 in 84ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:05:44.853Z',
variables: {}
}
POST /api/graphql 200 in 1367ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:05:45.521Z',
variables: {}
}
POST /api/graphql 200 in 459ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:05:46.030Z',
variables: {}
}
POST /api/graphql 200 in 495ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyEmployeesV2',
timestamp: '2025-09-10T16:05:46.778Z',
variables: {}
}
🔐 EMPLOYEE DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationId: 'cmfe5upy30001y56e4av0o4vs'
}
✅ AUTH PASSED: Calling resolver
🔐 EMPLOYEE DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationId: 'cmfe5upy30001y56e4av0o4vs'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_EMPLOYEES DOMAIN QUERY STARTED: { args: {}, userId: 'cmfe5lscj0000y56eeg95r8lr' }
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMySellerGoodsSupplyRequests',
timestamp: '2025-09-10T16:05:46.788Z',
variables: {}
}
🔐 SELLER GOODS DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationId: 'cmfe5upy30001y56e4av0o4vs'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_SELLER_GOODS_SUPPLY_REQUESTS DOMAIN QUERY STARTED: { userId: 'cmfe5lscj0000y56eeg95r8lr' }
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetLogisticsPartners',
timestamp: '2025-09-10T16:05:46.791Z',
variables: {}
}
❌ MY_SELLER_GOODS_SUPPLY_REQUESTS DOMAIN ERROR: GraphQLError: Доступно только для поставщиков
at checkWholesaleAccess (src/graphql/resolvers/domains/seller-goods.ts:95:10)
at async (src/graphql/resolvers/domains/seller-goods.ts:183:21)
at async Object.mySellerGoodsSupplyRequests (src/graphql/resolvers/domains/seller-goods.ts:30:21)
93 |
94 | if (!user.organization || user.organization.type !== 'WHOLESALE') {
> 95 | throw new GraphQLError('Доступно только для поставщиков', {
| ^
96 | extensions: { code: 'FORBIDDEN' },
97 | })
98 | } {
path: undefined,
locations: undefined,
extensions: [Object]
}
🎯 RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 1280ms
📦 LOGISTICS_PARTNERS RESOLVER CALLED: {
organizationId: 'cmfe5upy30001y56e4av0o4vs',
organizationType: 'FULFILLMENT',
timestamp: '2025-09-10T16:05:47.066Z'
}
📊 LOGISTICS_PARTNERS RESULT: { partnersCount: 0, organizationType: 'FULFILLMENT' }
POST /api/graphql 200 in 1446ms
✅ MY_EMPLOYEES DOMAIN SUCCESS: { total: 0, page: 1, employeesCount: 0 }
🎯 RESOLVER RESULT TYPE: object Has result
🎯 RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 1564ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNWxzY2owMDAweTU2ZWVnOTVyOGxyIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NzUxOTY5NiwiZXhwIjoxNzYwMTExNjk2fQ.W_r92r_4qh_HKhtKNfLmTHVyjl2E-eGXjTrkMndxkoY&orgId=cmfe5upy30001y56e4av0o4vs 200 in 663346ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNWxzY2owMDAweTU2ZWVnOTVyOGxyIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NzUxOTY5NiwiZXhwIjoxNzYwMTExNjk2fQ.W_r92r_4qh_HKhtKNfLmTHVyjl2E-eGXjTrkMndxkoY&orgId=cmfe5upy30001y56e4av0o4vs 200 in 24996ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNWxzY2owMDAweTU2ZWVnOTVyOGxyIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NzUxOTY5NiwiZXhwIjoxNzYwMTExNjk2fQ.W_r92r_4qh_HKhtKNfLmTHVyjl2E-eGXjTrkMndxkoY&orgId=cmfe5upy30001y56e4av0o4vs 200 in 24023ms
GET /fulfillment/supplies/goods/new 200 in 145ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 239ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:06:10.737Z',
variables: {}
}
POST /api/graphql 200 in 630ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:06:11.370Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:06:11.457Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:06:11.465Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:06:11.481Z',
variables: {}
}
POST /api/graphql 200 in 581ms
POST /api/graphql 200 in 669ms
📥 INCOMING_REQUESTS: {
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationId: 'cmfe5upy30001y56e4av0o4vs',
requestsCount: 0
}
POST /api/graphql 200 in 679ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyEmployeesV2',
timestamp: '2025-09-10T16:06:12.035Z',
variables: {}
}
🔐 EMPLOYEE DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationId: 'cmfe5upy30001y56e4av0o4vs'
}
✅ AUTH PASSED: Calling resolver
🔐 EMPLOYEE DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationId: 'cmfe5upy30001y56e4av0o4vs'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_EMPLOYEES DOMAIN QUERY STARTED: { args: {}, userId: 'cmfe5lscj0000y56eeg95r8lr' }
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:06:12.045Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMySellerGoodsSupplyRequests',
timestamp: '2025-09-10T16:06:12.056Z',
variables: {}
}
🔐 SELLER GOODS DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationId: 'cmfe5upy30001y56e4av0o4vs'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_SELLER_GOODS_SUPPLY_REQUESTS DOMAIN QUERY STARTED: { userId: 'cmfe5lscj0000y56eeg95r8lr' }
❌ MY_SELLER_GOODS_SUPPLY_REQUESTS DOMAIN ERROR: GraphQLError: Доступно только для поставщиков
at checkWholesaleAccess (src/graphql/resolvers/domains/seller-goods.ts:95:10)
at async (src/graphql/resolvers/domains/seller-goods.ts:183:21)
at async Object.mySellerGoodsSupplyRequests (src/graphql/resolvers/domains/seller-goods.ts:30:21)
93 |
94 | if (!user.organization || user.organization.type !== 'WHOLESALE') {
> 95 | throw new GraphQLError('Доступно только для поставщиков', {
| ^
96 | extensions: { code: 'FORBIDDEN' },
97 | })
98 | } {
path: undefined,
locations: undefined,
extensions: [Object]
}
🎯 RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 417ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1184ms
POST /api/graphql 200 in 480ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetLogisticsPartners',
timestamp: '2025-09-10T16:06:12.454Z',
variables: {}
}
✅ MY_EMPLOYEES DOMAIN SUCCESS: { total: 0, page: 1, employeesCount: 0 }
🎯 RESOLVER RESULT TYPE: object Has result
🎯 RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 710ms
📦 LOGISTICS_PARTNERS RESOLVER CALLED: {
organizationId: 'cmfe5upy30001y56e4av0o4vs',
organizationType: 'FULFILLMENT',
timestamp: '2025-09-10T16:06:12.615Z'
}
📊 LOGISTICS_PARTNERS RESULT: { partnersCount: 0, organizationType: 'FULFILLMENT' }
POST /api/graphql 200 in 536ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:06:13.500Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetConversations',
timestamp: '2025-09-10T16:06:13.502Z'
}
POST /api/graphql 200 in 78ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:06:13.512Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:06:13.512Z'
}
POST /api/graphql 200 in 87ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:06:13.551Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:06:13.551Z'
}
POST /api/graphql 200 in 47ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMySellerGoodsSupplyRequests',
timestamp: '2025-09-10T16:06:13.561Z',
variables: {}
}
🔐 SELLER GOODS DOMAIN AUTH CHECK: { hasUser: false, userId: undefined, organizationId: undefined }
❌ AUTH FAILED: No user in context
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMySellerGoodsSupplyRequests',
timestamp: '2025-09-10T16:06:13.562Z'
}
POST /api/graphql 200 in 46ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyEmployeesV2',
timestamp: '2025-09-10T16:06:13.604Z',
variables: {}
}
🔐 EMPLOYEE DOMAIN AUTH CHECK: { hasUser: false, userId: undefined, organizationId: undefined }
❌ AUTH FAILED: No user in context
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMyEmployeesV2',
timestamp: '2025-09-10T16:06:13.604Z'
}
POST /api/graphql 200 in 39ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetLogisticsPartners',
timestamp: '2025-09-10T16:06:13.615Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetLogisticsPartners',
timestamp: '2025-09-10T16:06:13.615Z'
}
POST /api/graphql 200 in 50ms
✓ Compiled / in 342ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNWxzY2owMDAweTU2ZWVnOTVyOGxyIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NzUxOTY5NiwiZXhwIjoxNzYwMTExNjk2fQ.W_r92r_4qh_HKhtKNfLmTHVyjl2E-eGXjTrkMndxkoY&orgId=cmfe5upy30001y56e4av0o4vs 200 in 2710ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNWxzY2owMDAweTU2ZWVnOTVyOGxyIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NzUxOTY5NiwiZXhwIjoxNzYwMTExNjk2fQ.W_r92r_4qh_HKhtKNfLmTHVyjl2E-eGXjTrkMndxkoY&orgId=cmfe5upy30001y56e4av0o4vs 200 in 1523ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNWxzY2owMDAweTU2ZWVnOTVyOGxyIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NzUxOTY5NiwiZXhwIjoxNzYwMTExNjk2fQ.W_r92r_4qh_HKhtKNfLmTHVyjl2E-eGXjTrkMndxkoY&orgId=cmfe5upy30001y56e4av0o4vs 200 in 2712ms
GET / 200 in 429ms
GET /login 200 in 43ms
GET /dashboard 200 in 38ms
GET /dashboard 200 in 33ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendSmsCode',
timestamp: '2025-09-10T16:06:19.504Z',
variables: { phone: '77777777777' }
}
POST /api/graphql 200 in 65ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifySmsCode',
timestamp: '2025-09-10T16:06:22.089Z',
variables: { phone: '77777777777', code: '1234' }
}
POST /api/graphql 200 in 382ms
GET /dashboard 200 in 91ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:06:22.819Z',
variables: {}
}
POST /api/graphql 200 in 443ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:06:23.318Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:06:23.327Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:06:23.338Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:06:23.341Z',
variables: {}
}
POST /api/graphql 200 in 475ms
POST /api/graphql 200 in 575ms
📥 INCOMING_REQUESTS: {
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationId: 'cmfe5upy30001y56e4av0o4vs',
requestsCount: 0
}
POST /api/graphql 200 in 577ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe5lscj0000y56eeg95r8lr',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1055ms
GET /dashboard 200 in 218ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:19.187Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:19.188Z'
}
POST /api/graphql 200 in 121ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 322ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:19.366Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:19.367Z'
}
POST /api/graphql 200 in 115ms
○ Compiling /_not-found/page ...
✓ Compiled /_not-found/page in 577ms
GET /apple-touch-icon-precomposed.png 404 in 665ms
GET /apple-touch-icon.png 404 in 52ms
GET /dashboard 200 in 101ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:24.108Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:24.109Z'
}
POST /api/graphql 200 in 63ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:24.185Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:24.185Z'
}
POST /api/graphql 200 in 49ms
GET / 200 in 101ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:25.542Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:25.543Z'
}
POST /api/graphql 200 in 55ms
GET /login 200 in 72ms
GET /dashboard 200 in 53ms
GET /dashboard 200 in 72ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:25.728Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:25.728Z'
}
POST /api/graphql 200 in 48ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:25.783Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:25.783Z'
}
POST /api/graphql 200 in 37ms
○ Compiling /admin/dashboard ...
✓ Compiled /admin/dashboard in 1158ms
GET /admin/dashboard 200 in 1231ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AdminMe',
timestamp: '2025-09-10T16:07:33.384Z',
variables: {}
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: false, adminId: undefined }
❌ ADMIN AUTH FAILED: No admin in context
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация администратора' ],
operationName: 'AdminMe',
timestamp: '2025-09-10T16:07:33.385Z'
}
POST /api/graphql 200 in 65ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:33.410Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:33.410Z'
}
POST /api/graphql 200 in 82ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AdminMe',
timestamp: '2025-09-10T16:07:33.444Z',
variables: {}
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: false, adminId: undefined }
❌ ADMIN AUTH FAILED: No admin in context
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация администратора' ],
operationName: 'AdminMe',
timestamp: '2025-09-10T16:07:33.445Z'
}
POST /api/graphql 200 in 33ms
✓ Compiled /admin in 315ms
GET /admin 307 in 383ms
GET /admin/dashboard 200 in 66ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AdminMe',
timestamp: '2025-09-10T16:07:36.398Z',
variables: {}
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: false, adminId: undefined }
❌ ADMIN AUTH FAILED: No admin in context
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация администратора' ],
operationName: 'AdminMe',
timestamp: '2025-09-10T16:07:36.399Z'
}
POST /api/graphql 200 in 66ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:36.412Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:07:36.412Z'
}
POST /api/graphql 200 in 74ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AdminMe',
timestamp: '2025-09-10T16:07:36.440Z',
variables: {}
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: false, adminId: undefined }
❌ ADMIN AUTH FAILED: No admin in context
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация администратора' ],
operationName: 'AdminMe',
timestamp: '2025-09-10T16:07:36.440Z'
}
POST /api/graphql 200 in 25ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'AdminLogin',
timestamp: '2025-09-10T16:07:39.283Z',
variables: { username: 'admin', password: 'admin123' }
}
🔍 ADMIN_LOGIN DOMAIN MUTATION STARTED: { username: 'admin', hasPassword: true }
❌ ADMIN_LOGIN DOMAIN ERROR: Error [PrismaClientValidationError]:
Invalid `__TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["prisma"].admin.update()` invocation in
/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/chunks/[root-of-the-server]__5933e5dd._.js:11772:158
11769 };
11770 }
11771 // Обновление последнего входа
→ 11772 await __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["prisma"].admin.update({
where: {
id: "cmfbgb20b0000y5ley71phryx"
},
data: {
lastLoginAt: new Date("2025-09-10T16:07:39.870Z"),
~~~~~~~~~~~
? id?: String | StringFieldUpdateOperationsInput,
? username?: String | StringFieldUpdateOperationsInput,
? password?: String | StringFieldUpdateOperationsInput,
? email?: String | NullableStringFieldUpdateOperationsInput | Null,
? isActive?: Boolean | BoolFieldUpdateOperationsInput,
? lastLogin?: DateTime | NullableDateTimeFieldUpdateOperationsInput | Null,
? createdAt?: DateTime | DateTimeFieldUpdateOperationsInput,
? updatedAt?: DateTime | DateTimeFieldUpdateOperationsInput
}
})
Unknown argument `lastLoginAt`. Did you mean `lastLogin`? Available options are marked with ?.
at <unknown> (src/graphql/resolvers/domains/admin-tools.ts:231:27)
at async Object.adminLogin (src/graphql/resolvers/domains/admin-tools.ts:231:8)
229 |
230 | // Обновление последнего входа
> 231 | await prisma.admin.update({
| ^
232 | where: { id: admin.id },
233 | data: { lastLoginAt: new Date() },
234 | }) {
clientVersion: '6.12.0'
}
POST /api/graphql 200 in 702ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'AdminLogin',
timestamp: '2025-09-10T16:07:55.580Z',
variables: { username: 'admin', password: 'admin123' }
}
🔍 ADMIN_LOGIN DOMAIN MUTATION STARTED: { username: 'admin', hasPassword: true }
❌ ADMIN_LOGIN DOMAIN ERROR: Error [PrismaClientValidationError]:
Invalid `__TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["prisma"].admin.update()` invocation in
/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/chunks/[root-of-the-server]__5933e5dd._.js:11772:158
11769 };
11770 }
11771 // Обновление последнего входа
→ 11772 await __TURBOPACK__imported__module__$5b$project$5d2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["prisma"].admin.update({
where: {
id: "cmfbgb20b0000y5ley71phryx"
},
data: {
lastLoginAt: new Date("2025-09-10T16:07:56.066Z"),
~~~~~~~~~~~
? id?: String | StringFieldUpdateOperationsInput,
? username?: String | StringFieldUpdateOperationsInput,
? password?: String | StringFieldUpdateOperationsInput,
? email?: String | NullableStringFieldUpdateOperationsInput | Null,
? isActive?: Boolean | BoolFieldUpdateOperationsInput,
? lastLogin?: DateTime | NullableDateTimeFieldUpdateOperationsInput | Null,
? createdAt?: DateTime | DateTimeFieldUpdateOperationsInput,
? updatedAt?: DateTime | DateTimeFieldUpdateOperationsInput
}
})
Unknown argument `lastLoginAt`. Did you mean `lastLogin`? Available options are marked with ?.
at <unknown> (src/graphql/resolvers/domains/admin-tools.ts:231:27)
at async Object.adminLogin (src/graphql/resolvers/domains/admin-tools.ts:231:8)
229 |
230 | // Обновление последнего входа
> 231 | await prisma.admin.update({
| ^
232 | where: { id: admin.id },
233 | data: { lastLoginAt: new Date() },
234 | }) {
clientVersion: '6.12.0'
}
POST /api/graphql 200 in 667ms
GET /admin/dashboard 200 in 133ms
🔥 МОДУЛЬ SERVICES DOMAIN ЗАГРУЖАЕТСЯ
🔥 SERVICES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 INVENTORY DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER GOODS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 LOGISTICS CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 WILDBERRIES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ANALYTICS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ADMIN TOOLS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 FILE MANAGEMENT DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 EXTERNAL ADS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:09:21.921Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AdminMe',
timestamp: '2025-09-10T16:09:21.922Z',
variables: {}
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: false, adminId: undefined }
❌ ADMIN AUTH FAILED: No admin in context
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:09:21.924Z'
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация администратора' ],
operationName: 'AdminMe',
timestamp: '2025-09-10T16:09:21.924Z'
}
POST /api/graphql 200 in 422ms
POST /api/graphql 200 in 422ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AdminMe',
timestamp: '2025-09-10T16:09:21.989Z',
variables: {}
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: false, adminId: undefined }
❌ ADMIN AUTH FAILED: No admin in context
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация администратора' ],
operationName: 'AdminMe',
timestamp: '2025-09-10T16:09:21.989Z'
}
POST /api/graphql 200 in 33ms
GraphQL Context - Invalid token: Error [TokenExpiredError]: jwt expired
at context (src/app/api/graphql/route.ts:73:26)
at ApolloServer.executeHTTPGraphQLRequest (../../src/ApolloServer.ts:1083:29)
71 | }
72 |
> 73 | const decoded = jwt.verify(token, jwtSecret) as {
| ^
74 | userId?: string
75 | phone?: string
76 | adminId?: string {
expiredAt: 2025-08-26T10:21:54.000Z,
constructor: [Function: TokenExpiredError]
}
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'AdminLogin',
timestamp: '2025-09-10T16:09:24.449Z',
variables: { username: 'admin', password: 'admin123' }
}
🔍 ADMIN_LOGIN DOMAIN MUTATION STARTED: { username: 'admin', hasPassword: true }
✅ ADMIN_LOGIN DOMAIN SUCCESS: {
adminId: 'cmfbgb20b0000y5ley71phryx',
username: 'admin',
tokenGenerated: true
}
POST /api/graphql 200 in 774ms
GET /admin/dashboard 200 in 87ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AdminMe',
timestamp: '2025-09-10T16:09:25.385Z',
variables: {}
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: true, adminId: 'cmfbgb20b0000y5ley71phryx' }
✅ ADMIN AUTH PASSED: Calling resolver
🔍 ADMIN_ME DOMAIN QUERY STARTED: { adminId: 'cmfbgb20b0000y5ley71phryx' }
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:09:25.393Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:09:25.394Z'
}
POST /api/graphql 200 in 68ms
✅ ADMIN_ME DOMAIN SUCCESS: {
adminId: 'cmfbgb20b0000y5ley71phryx',
username: 'admin',
isActive: true
}
🎯 ADMIN RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 224ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AdminMe',
timestamp: '2025-09-10T16:09:25.921Z',
variables: {}
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: true, adminId: 'cmfbgb20b0000y5ley71phryx' }
✅ ADMIN AUTH PASSED: Calling resolver
🔍 ADMIN_ME DOMAIN QUERY STARTED: { adminId: 'cmfbgb20b0000y5ley71phryx' }
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AllUsers',
timestamp: '2025-09-10T16:09:25.935Z',
variables: { limit: 20, offset: 0 }
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: true, adminId: 'cmfbgb20b0000y5ley71phryx' }
✅ ADMIN AUTH PASSED: Calling resolver
🔍 ALL_USERS DOMAIN QUERY STARTED: {
adminId: 'cmfbgb20b0000y5ley71phryx',
search: undefined,
limit: 20,
offset: 0
}
✅ ADMIN_ME DOMAIN SUCCESS: {
adminId: 'cmfbgb20b0000y5ley71phryx',
username: 'admin',
isActive: true
}
🎯 ADMIN RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 130ms
✅ ALL_USERS DOMAIN SUCCESS: {
adminId: 'cmfbgb20b0000y5ley71phryx',
usersFound: 3,
totalCount: 3,
hasSearch: false
}
🎯 ADMIN RESOLVER RESULT TYPE: object Has result
❌ GraphQL ERROR: {
errors: [ 'Cannot return null for non-nullable field UsersResponse.total.' ],
operationName: 'AllUsers',
timestamp: '2025-09-10T16:09:26.505Z'
}
POST /api/graphql 200 in 632ms
GET /admin/dashboard 200 in 125ms
🔥 МОДУЛЬ SERVICES DOMAIN ЗАГРУЖАЕТСЯ
🔥 SERVICES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 INVENTORY DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER GOODS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 LOGISTICS CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 WILDBERRIES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ANALYTICS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ADMIN TOOLS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 FILE MANAGEMENT DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 EXTERNAL ADS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AdminMe',
timestamp: '2025-09-10T16:10:29.402Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:10:29.402Z',
variables: {}
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: true, adminId: 'cmfbgb20b0000y5ley71phryx' }
✅ ADMIN AUTH PASSED: Calling resolver
🔍 ADMIN_ME DOMAIN QUERY STARTED: { adminId: 'cmfbgb20b0000y5ley71phryx' }
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T16:10:29.407Z'
}
POST /api/graphql 200 in 359ms
✅ ADMIN_ME DOMAIN SUCCESS: {
adminId: 'cmfbgb20b0000y5ley71phryx',
username: 'admin',
isActive: true
}
🎯 ADMIN RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 615ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AdminMe',
timestamp: '2025-09-10T16:10:30.081Z',
variables: {}
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: true, adminId: 'cmfbgb20b0000y5ley71phryx' }
✅ ADMIN AUTH PASSED: Calling resolver
🔍 ADMIN_ME DOMAIN QUERY STARTED: { adminId: 'cmfbgb20b0000y5ley71phryx' }
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'AllUsers',
timestamp: '2025-09-10T16:10:30.097Z',
variables: { limit: 20, offset: 0 }
}
🔐 ADMIN TOOLS DOMAIN ADMIN AUTH CHECK: { hasAdmin: true, adminId: 'cmfbgb20b0000y5ley71phryx' }
✅ ADMIN AUTH PASSED: Calling resolver
🔍 ALL_USERS DOMAIN QUERY STARTED: {
adminId: 'cmfbgb20b0000y5ley71phryx',
search: undefined,
limit: 20,
offset: 0
}
✅ ADMIN_ME DOMAIN SUCCESS: {
adminId: 'cmfbgb20b0000y5ley71phryx',
username: 'admin',
isActive: true
}
🎯 ADMIN RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 173ms
✅ ALL_USERS DOMAIN SUCCESS: {
adminId: 'cmfbgb20b0000y5ley71phryx',
usersFound: 3,
totalCount: 3,
hasSearch: false
}
🎯 ADMIN RESOLVER RESULT TYPE: object Has result
POST /api/graphql 200 in 676ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:10:54.401Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:10:54.401Z'
}
POST /api/graphql 200 in 120ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:10:54.420Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetConversations',
timestamp: '2025-09-10T16:10:54.420Z'
}
POST /api/graphql 200 in 139ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:10:54.432Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:10:54.433Z'
}
POST /api/graphql 200 in 150ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNWxzY2owMDAweTU2ZWVnOTVyOGxyIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NyIsImlhdCI6MTc1NzUyMDM4MiwiZXhwIjoxNzYwMTEyMzgyfQ.fOiRqZLoLl_w1Er38rnyqDILi9nc8149RIajViR_vMI&orgId=cmfe5upy30001y56e4av0o4vs 200 in 271381ms
GET / 200 in 211ms
GET /login 200 in 56ms
GET /dashboard 200 in 50ms
GET /dashboard 200 in 48ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendSmsCode',
timestamp: '2025-09-10T16:11:03.206Z',
variables: { phone: '77777777771' }
}
POST /api/graphql 200 in 85ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifySmsCode',
timestamp: '2025-09-10T16:11:05.582Z',
variables: { phone: '77777777771', code: '1234' }
}
POST /api/graphql 200 in 1338ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T16:11:16.777Z',
variables: { inn: '7743291031' }
}
🔍 VERIFY_INN STARTED: { inn: '7743291031' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '7743291031', name: 'А-Я ЛОГИСТИКА', isActive: true }
POST /api/graphql 200 in 665ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:11:18.875Z',
variables: {}
}
POST /api/graphql 200 in 238ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'RegisterFulfillmentOrganization',
timestamp: '2025-09-10T16:11:23.060Z',
variables: {
input: {
phone: '77777777771',
inn: '7743291031',
type: 'LOGIST',
referralCode: null,
partnerCode: null
}
}
}
🏢 REGISTER_FULFILLMENT_ORGANIZATION - ВЫЗВАН: {
phone: '77777777771',
inn: '7743291031',
referralCode: null,
timestamp: '2025-09-10T16:11:23.061Z'
}
POST /api/graphql 200 in 404ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T16:12:02.667Z',
variables: { inn: '7814632989' }
}
🔍 VERIFY_INN STARTED: { inn: '7814632989' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '7814632989', name: 'ЛОГИСТИКА', isActive: true }
POST /api/graphql 200 in 554ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:12:04.645Z',
variables: {}
}
POST /api/graphql 200 in 241ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'RegisterFulfillmentOrganization',
timestamp: '2025-09-10T16:12:08.130Z',
variables: {
input: {
phone: '77777777771',
inn: '7814632989',
type: 'LOGIST',
referralCode: null,
partnerCode: null
}
}
}
🏢 REGISTER_FULFILLMENT_ORGANIZATION - ВЫЗВАН: {
phone: '77777777771',
inn: '7814632989',
referralCode: null,
timestamp: '2025-09-10T16:12:08.131Z'
}
✅ ФУЛФИЛМЕНТ ОРГАНИЗАЦИЯ СОЗДАНА: {
organizationId: 'cmfe6gp2e0003y56ez626euyz',
userId: 'cmfe6fd9b0002y56etpt2sywd',
inn: '7814632989',
type: 'FULFILLMENT',
referralCode: 'FF_7814632989_1757520728293'
}
POST /api/graphql 200 in 1602ms
GET /dashboard 200 in 139ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:12:12.166Z',
variables: {}
}
POST /api/graphql 200 in 540ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:12:12.700Z',
variables: {}
}
POST /api/graphql 200 in 498ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:12:13.478Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:12:13.494Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:12:13.496Z',
variables: {}
}
POST /api/graphql 200 in 1446ms
📥 INCOMING_REQUESTS: {
userId: 'cmfe6fd9b0002y56etpt2sywd',
organizationId: 'cmfe6gp2e0003y56ez626euyz',
requestsCount: 0
}
POST /api/graphql 200 in 1460ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe6fd9b0002y56etpt2sywd',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1949ms
○ Compiling /fulfillment/settings ...
✓ Compiled /fulfillment/settings in 613ms
GET /fulfillment/settings 200 in 646ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:13:06.335Z',
variables: {}
}
POST /api/graphql 200 in 618ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:13:06.943Z',
variables: {}
}
POST /api/graphql 200 in 473ms
✓ Compiled in 529ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNmZkOWIwMDAyeTU2ZXRwdDJzeXdkIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3MSIsImlhdCI6MTc1NzUyMDY2NiwiZXhwIjoxNzYwMTEyNjY2fQ._P4xu-3dXaq3VSUpYZBE1QrtgPNhK0VQQ0p7EjU8XLE&orgId=cmfe6gp2e0003y56ez626euyz 200 in 302058ms
GET /fulfillment/settings 200 in 225ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 314ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:17:15.768Z',
variables: {}
}
POST /api/graphql 200 in 1431ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:17:16.497Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:17:17.241Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:17:17.254Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:17:17.258Z',
variables: {}
}
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe6fd9b0002y56etpt2sywd',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1072ms
POST /api/graphql 200 in 1412ms
POST /api/graphql 200 in 1417ms
📥 INCOMING_REQUESTS: {
userId: 'cmfe6fd9b0002y56etpt2sywd',
organizationId: 'cmfe6gp2e0003y56ez626euyz',
requestsCount: 0
}
POST /api/graphql 200 in 1422ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:17:20.628Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetConversations',
timestamp: '2025-09-10T16:17:20.629Z'
}
POST /api/graphql 200 in 92ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:17:20.637Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:17:20.637Z'
}
POST /api/graphql 200 in 100ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:17:20.649Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:17:20.650Z'
}
POST /api/graphql 200 in 112ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNmZkOWIwMDAyeTU2ZXRwdDJzeXdkIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3MSIsImlhdCI6MTc1NzUyMDY2NiwiZXhwIjoxNzYwMTEyNjY2fQ._P4xu-3dXaq3VSUpYZBE1QrtgPNhK0VQQ0p7EjU8XLE&orgId=cmfe6gp2e0003y56ez626euyz 200 in 4447ms
GET / 200 in 189ms
GET /login 200 in 54ms
GET /dashboard 200 in 141ms
GET /dashboard 200 in 49ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendSmsCode',
timestamp: '2025-09-10T16:17:28.125Z',
variables: { phone: '77777777772' }
}
POST /api/graphql 200 in 70ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifySmsCode',
timestamp: '2025-09-10T16:17:30.936Z',
variables: { phone: '77777777772', code: '1234' }
}
POST /api/graphql 200 in 730ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T16:17:52.476Z',
variables: { inn: '5074056720' }
}
🔍 VERIFY_INN STARTED: { inn: '5074056720' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '5074056720', name: 'ЛОГИСТИКА+', isActive: true }
POST /api/graphql 200 in 733ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:17:54.530Z',
variables: {}
}
POST /api/graphql 200 in 225ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'RegisterFulfillmentOrganization',
timestamp: '2025-09-10T16:17:55.709Z',
variables: {
input: {
phone: '77777777772',
inn: '5074056720',
type: 'LOGIST',
referralCode: null,
partnerCode: null
}
}
}
🏢 REGISTER_FULFILLMENT_ORGANIZATION - ВЫЗВАН: {
phone: '77777777772',
inn: '5074056720',
referralCode: null,
timestamp: '2025-09-10T16:17:55.710Z'
}
✅ ФУЛФИЛМЕНТ ОРГАНИЗАЦИЯ СОЗДАНА: {
organizationId: 'cmfe6o5bs0005y56egre5lbie',
userId: 'cmfe6nm570004y56ep1dg7jyp',
inn: '5074056720',
type: 'FULFILLMENT',
referralCode: 'FF_5074056720_1757521075958'
}
POST /api/graphql 200 in 1601ms
GET /dashboard 200 in 289ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:17:59.838Z',
variables: {}
}
POST /api/graphql 200 in 462ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:18:00.374Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:18:00.471Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:18:00.475Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:18:00.479Z',
variables: {}
}
📥 INCOMING_REQUESTS: {
userId: 'cmfe6nm570004y56ep1dg7jyp',
organizationId: 'cmfe6o5bs0005y56egre5lbie',
requestsCount: 0
}
POST /api/graphql 200 in 500ms
POST /api/graphql 200 in 598ms
POST /api/graphql 200 in 683ms
✓ Compiled /fulfillment/home in 412ms
GET /fulfillment/home 200 in 453ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe6nm570004y56ep1dg7jyp',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1161ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:18:01.465Z',
variables: {}
}
POST /api/graphql 200 in 544ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:18:02.054Z',
variables: {}
}
POST /api/graphql 200 in 494ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNm5tNTcwMDA0eTU2ZXAxZGc3anlwIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3MiIsImlhdCI6MTc1NzUyMTA1MSwiZXhwIjoxNzYwMTEzMDUxfQ.w8grW5wYNYCJQshrTweAmra_-jCW9zMJJK3K8bo31gU&orgId=cmfe6o5bs0005y56egre5lbie 200 in 5219ms
GET /fulfillment/home 200 in 152ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:18:05.690Z',
variables: {}
}
GET /favicon.ico?favicon.45db1c09.ico 200 in 248ms
POST /api/graphql 200 in 466ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:18:06.227Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:18:06.235Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:18:06.246Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:18:06.252Z',
variables: {}
}
POST /api/graphql 200 in 529ms
📥 INCOMING_REQUESTS: {
userId: 'cmfe6nm570004y56ep1dg7jyp',
organizationId: 'cmfe6o5bs0005y56egre5lbie',
requestsCount: 0
}
POST /api/graphql 200 in 589ms
POST /api/graphql 200 in 608ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe6nm570004y56ep1dg7jyp',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 756ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:18:33.426Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetConversations',
timestamp: '2025-09-10T16:18:33.427Z'
}
POST /api/graphql 200 in 138ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:18:33.448Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:18:33.449Z'
}
POST /api/graphql 200 in 153ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:18:33.459Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:18:33.459Z'
}
POST /api/graphql 200 in 169ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNm5tNTcwMDA0eTU2ZXAxZGc3anlwIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3MiIsImlhdCI6MTc1NzUyMTA1MSwiZXhwIjoxNzYwMTEzMDUxfQ.w8grW5wYNYCJQshrTweAmra_-jCW9zMJJK3K8bo31gU&orgId=cmfe6o5bs0005y56egre5lbie 200 in 27525ms
GET / 200 in 251ms
GET /login 200 in 45ms
GET /dashboard 200 in 53ms
GET /dashboard 200 in 61ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendSmsCode',
timestamp: '2025-09-10T16:18:45.427Z',
variables: { phone: '77777777773' }
}
POST /api/graphql 200 in 91ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifySmsCode',
timestamp: '2025-09-10T16:18:47.636Z',
variables: { phone: '77777777773', code: '1234' }
}
POST /api/graphql 200 in 808ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T16:19:03.781Z',
variables: { inn: '7713726983' }
}
🔍 VERIFY_INN STARTED: { inn: '7713726983' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '7713726983', name: 'ЛОГИСТИКА', isActive: true }
POST /api/graphql 200 in 564ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:19:05.778Z',
variables: {}
}
POST /api/graphql 200 in 255ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'RegisterFulfillmentOrganization',
timestamp: '2025-09-10T16:19:08.549Z',
variables: {
input: {
phone: '77777777773',
inn: '7713726983',
type: 'LOGIST',
referralCode: null,
partnerCode: null
}
}
}
🏢 REGISTER_FULFILLMENT_ORGANIZATION - ВЫЗВАН: {
phone: '77777777773',
inn: '7713726983',
referralCode: null,
timestamp: '2025-09-10T16:19:08.549Z'
}
✅ ФУЛФИЛМЕНТ ОРГАНИЗАЦИЯ СОЗДАНА: {
organizationId: 'cmfe6ppiz0007y56efxzjf3qs',
userId: 'cmfe6p9dz0006y56esdge2glp',
inn: '7713726983',
type: 'FULFILLMENT',
referralCode: 'FF_7713726983_1757521148794'
}
POST /api/graphql 200 in 1534ms
GET /dashboard 200 in 135ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:19:12.402Z',
variables: {}
}
POST /api/graphql 200 in 465ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:19:12.947Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:19:13.039Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:19:13.042Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:19:13.057Z',
variables: {}
}
POST /api/graphql 200 in 508ms
POST /api/graphql 200 in 605ms
📥 INCOMING_REQUESTS: {
userId: 'cmfe6p9dz0006y56esdge2glp',
organizationId: 'cmfe6ppiz0007y56efxzjf3qs',
requestsCount: 0
}
POST /api/graphql 200 in 606ms
GET /fulfillment/home 200 in 85ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:19:13.551Z',
variables: {}
}
POST /api/graphql 200 in 455ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe6p9dz0006y56esdge2glp',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1200ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:19:14.044Z',
variables: {}
}
POST /api/graphql 200 in 482ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZiZ205YzkwMDA0eTVuYXF6dzc2YnhkIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NzM1NjM4NSwiZXhwIjoxNzU5OTQ4Mzg1fQ.OmzvmEGZ5hOrKciYnLRqZWj6KpiQtaTnUa43iaU7Rkc&orgId=cmfbgmqer0005y5na1ezlc4aw 200 in 2604748ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZiZ205YzkwMDA0eTVuYXF6dzc2YnhkIiwicGhvbmUiOiI3ODg4ODg4ODg4OCIsImlhdCI6MTc1NzM1NjM4NSwiZXhwIjoxNzU5OTQ4Mzg1fQ.OmzvmEGZ5hOrKciYnLRqZWj6KpiQtaTnUa43iaU7Rkc&orgId=cmfbgmqer0005y5na1ezlc4aw 200 in 2608508ms
GET /fulfillment/home 200 in 103ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNnA5ZHowMDA2eTU2ZXNkZ2UyZ2xwIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3MyIsImlhdCI6MTc1NzUyMTEyOCwiZXhwIjoxNzYwMTEzMTI4fQ.7BwgoLHMtaotV_46AI87usRsIRGzUPeD4tIGpcnmKXU&orgId=cmfe6ppiz0007y56efxzjf3qs 200 in 2264340ms
🔥 МОДУЛЬ SERVICES DOMAIN ЗАГРУЖАЕТСЯ
🔥 SERVICES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 INVENTORY DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER GOODS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 LOGISTICS CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 WILDBERRIES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ANALYTICS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ADMIN TOOLS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 FILE MANAGEMENT DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 EXTERNAL ADS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
GET /favicon.ico?favicon.45db1c09.ico 200 in 516ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:56:58.426Z',
variables: {}
}
POST /api/graphql 200 in 1691ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:56:59.127Z',
variables: {}
}
POST /api/graphql 200 in 493ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:56:59.884Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:56:59.894Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:56:59.936Z',
variables: {}
}
📥 INCOMING_REQUESTS: {
userId: 'cmfe6p9dz0006y56esdge2glp',
organizationId: 'cmfe6ppiz0007y56efxzjf3qs',
requestsCount: 0
}
POST /api/graphql 200 in 1410ms
POST /api/graphql 200 in 1479ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe6p9dz0006y56esdge2glp',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1900ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:57:01.043Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:57:01.046Z'
}
POST /api/graphql 200 in 77ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:57:01.053Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:57:01.053Z'
}
POST /api/graphql 200 in 84ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:57:01.060Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetConversations',
timestamp: '2025-09-10T16:57:01.061Z'
}
POST /api/graphql 200 in 91ms
GET / 200 in 109ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlNnA5ZHowMDA2eTU2ZXNkZ2UyZ2xwIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3MyIsImlhdCI6MTc1NzUyMTEyOCwiZXhwIjoxNzYwMTEzMTI4fQ.7BwgoLHMtaotV_46AI87usRsIRGzUPeD4tIGpcnmKXU&orgId=cmfe6ppiz0007y56efxzjf3qs 200 in 2194ms
GET /login 200 in 29ms
GET /dashboard 200 in 32ms
GET /dashboard 200 in 30ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendSmsCode',
timestamp: '2025-09-10T16:57:08.363Z',
variables: { phone: '77777777774' }
}
POST /api/graphql 200 in 59ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifySmsCode',
timestamp: '2025-09-10T16:57:10.618Z',
variables: { phone: '77777777774', code: '1234' }
}
POST /api/graphql 200 in 713ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T16:57:36.167Z',
variables: { inn: '5010050095' }
}
🔍 VERIFY_INN STARTED: { inn: '5010050095' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '5010050095', name: 'ЛОГИСТИКА', isActive: true }
POST /api/graphql 200 in 676ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:57:38.280Z',
variables: {}
}
POST /api/graphql 200 in 233ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'RegisterFulfillmentOrganization',
timestamp: '2025-09-10T16:57:39.326Z',
variables: {
input: {
phone: '77777777774',
inn: '5010050095',
type: 'LOGIST',
referralCode: null,
partnerCode: null
}
}
}
🏢 REGISTER_FULFILLMENT_ORGANIZATION - ВЫЗВАН: {
phone: '77777777774',
inn: '5010050095',
referralCode: null,
timestamp: '2025-09-10T16:57:39.327Z'
}
✅ ФУЛФИЛМЕНТ ОРГАНИЗАЦИЯ СОЗДАНА: {
organizationId: 'cmfe838ja0009y56ed0rk8us5',
userId: 'cmfe82mbq0008y56eyd3czwal',
inn: '5010050095',
type: 'LOGIST',
referralCode: 'FF_5010050095_1757523459573'
}
POST /api/graphql 200 in 1605ms
GET /dashboard 200 in 91ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:57:43.175Z',
variables: {}
}
POST /api/graphql 200 in 439ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T16:57:43.693Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T16:57:43.786Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T16:57:43.791Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:57:43.794Z',
variables: {}
}
POST /api/graphql 200 in 560ms
📥 INCOMING_REQUESTS: {
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationId: 'cmfe838ja0009y56ed0rk8us5',
requestsCount: 0
}
POST /api/graphql 200 in 575ms
POST /api/graphql 200 in 579ms
✓ Compiled /logistics/home in 321ms
GET /logistics/home 200 in 350ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationType: 'LOGIST',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1170ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:57:44.635Z',
variables: {}
}
POST /api/graphql 200 in 433ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T16:57:45.121Z',
variables: {}
}
POST /api/graphql 200 in 472ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZiZ2gyd2wwMDAxeTVuYXAyNGZhc3VpIiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NzQwNzQzMCwiZXhwIjoxNzU5OTk5NDMwfQ.jw0t2qqwtuqBPbzJZ71iLim623iK4y8XCRtbByg8-Lw&orgId=cmfbghrno0002y5na8b59ykfr 200 in 4226001ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZiZ2gyd2wwMDAxeTVuYXAyNGZhc3VpIiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NzQwNzQzMCwiZXhwIjoxNzU5OTk5NDMwfQ.jw0t2qqwtuqBPbzJZ71iLim623iK4y8XCRtbByg8-Lw&orgId=cmfbghrno0002y5na8b59ykfr 200 in 4244214ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlODJtYnEwMDA4eTU2ZXlkM2N6d2FsIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NCIsImlhdCI6MTc1NzUyMzQzMSwiZXhwIjoxNzYwMTE1NDMxfQ.5hfVBi7WHoAkPssQupj3pnDMkCyo1YcvncLuJv10Tgo&orgId=cmfe838ja0009y56ed0rk8us5 200 in 6785ms
✓ Compiled /logistics/partners in 383ms
GET /logistics/partners 200 in 423ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:10:47.203Z',
variables: {}
}
POST /api/graphql 200 in 1450ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyPartnerLink',
timestamp: '2025-09-10T18:10:47.963Z',
variables: {}
}
🔐 REFERRALS DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationId: 'cmfe838ja0009y56ed0rk8us5'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_PARTNER_LINK DOMAIN QUERY STARTED: { userId: 'cmfe82mbq0008y56eyd3czwal' }
✅ MY_PARTNER_LINK DOMAIN SUCCESS: {
link: 'http://localhost:3000/register?partner=FF_5010050095_1757523459573'
}
🎯 RESOLVER RESULT TYPE: string Has result
POST /api/graphql 200 in 587ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyCounterparties',
timestamp: '2025-09-10T18:10:48.812Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:10:48.824Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetOutgoingRequests',
timestamp: '2025-09-10T18:10:48.828Z',
variables: {}
}
🤝 MY_COUNTERPARTIES: {
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationId: 'cmfe838ja0009y56ed0rk8us5',
organizationType: 'LOGIST',
counterpartiesCount: 0
}
POST /api/graphql 200 in 1514ms
POST /api/graphql 200 in 1523ms
📤 OUTGOING_REQUESTS: {
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationId: 'cmfe838ja0009y56ed0rk8us5',
requestsCount: 0
}
POST /api/graphql 200 in 1529ms
🔍 RegisterContent - URL параметры: {
partnerCode: 'FF_5010050095_1757523459573',
referralCode: null,
searchParams: { partner: 'FF_5010050095_1757523459573' }
}
Недействительный партнерский код: FF_5010050095_1757523459573
GET /register?partner=FF_5010050095_1757523459573 200 in 102ms
🔍 RegisterContent - URL параметры: { partnerCode: null, referralCode: null, searchParams: {} }
🚀 RegisterContent - Передача в AuthFlow: { partnerCode: null, referralCode: null }
GET /favicon.ico?favicon.45db1c09.ico 200 in 96ms
GET /register 200 in 90ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 50ms
GET /dashboard 200 in 51ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 226ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendSmsCode',
timestamp: '2025-09-10T18:11:33.188Z',
variables: { phone: '76666666666' }
}
POST /api/graphql 200 in 60ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifySmsCode',
timestamp: '2025-09-10T18:11:35.478Z',
variables: { phone: '76666666666', code: '1234' }
}
POST /api/graphql 200 in 809ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T18:12:13.217Z',
variables: { inn: '7729706894' }
}
🔍 VERIFY_INN STARTED: { inn: '7729706894' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '7729706894', name: 'ПОСТАВЩИК', isActive: true }
POST /api/graphql 200 in 619ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:12:15.275Z',
variables: {}
}
POST /api/graphql 200 in 216ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'RegisterFulfillmentOrganization',
timestamp: '2025-09-10T18:12:17.651Z',
variables: {
input: {
phone: '76666666666',
inn: '7729706894',
type: 'WHOLESALE',
referralCode: null,
partnerCode: null
}
}
}
🏢 REGISTER_FULFILLMENT_ORGANIZATION - ВЫЗВАН: {
phone: '76666666666',
inn: '7729706894',
referralCode: null,
timestamp: '2025-09-10T18:12:17.652Z'
}
✅ ФУЛФИЛМЕНТ ОРГАНИЗАЦИЯ СОЗДАНА: {
organizationId: 'cmfear815000by56e454o1xs6',
userId: 'cmfeaqbhy000ay56e0dhso6f9',
inn: '7729706894',
type: 'WHOLESALE',
referralCode: 'FF_7729706894_1757527937896'
}
POST /api/graphql 200 in 1595ms
GET /dashboard 200 in 81ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:12:21.500Z',
variables: {}
}
GET /favicon.ico?favicon.45db1c09.ico 200 in 238ms
POST /api/graphql 200 in 444ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:12:22.019Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T18:12:22.109Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T18:12:22.113Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T18:12:22.119Z',
variables: {}
}
POST /api/graphql 200 in 485ms
📥 INCOMING_REQUESTS: {
userId: 'cmfeaqbhy000ay56e0dhso6f9',
organizationId: 'cmfear815000by56e454o1xs6',
requestsCount: 0
}
POST /api/graphql 200 in 656ms
POST /api/graphql 200 in 664ms
✓ Compiled /wholesale/home in 260ms
GET /wholesale/home 200 in 288ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:12:22.843Z',
variables: {}
}
GET /favicon.ico?favicon.45db1c09.ico 200 in 278ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfeaqbhy000ay56e0dhso6f9',
organizationType: 'WHOLESALE',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1146ms
POST /api/graphql 200 in 478ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:12:23.323Z',
variables: {}
}
POST /api/graphql 200 in 475ms
✓ Compiled /logistics/settings in 436ms
GET /logistics/settings 200 in 472ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlODJtYnEwMDA4eTU2ZXlkM2N6d2FsIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NCIsImlhdCI6MTc1NzUyMzQzMSwiZXhwIjoxNzYwMTE1NDMxfQ.5hfVBi7WHoAkPssQupj3pnDMkCyo1YcvncLuJv10Tgo&orgId=cmfe838ja0009y56ed0rk8us5 200 in 141689ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:13:09.711Z',
variables: {}
}
POST /api/graphql 200 in 553ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:13:10.232Z',
variables: {}
}
POST /api/graphql 200 in 466ms
GET /logistics/partners 200 in 51ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:18:59.904Z',
variables: {}
}
POST /api/graphql 200 in 1354ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:19:00.600Z',
variables: {}
}
POST /api/graphql 200 in 465ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'SearchOrganizations',
timestamp: '2025-09-10T18:19:01.922Z',
variables: { type: 'SELLER', search: null }
}
POST /api/graphql 200 in 1112ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendCounterpartyRequest',
timestamp: '2025-09-10T18:19:05.332Z',
variables: {
organizationId: 'cmfbgmqer0005y5na1ezlc4aw',
message: 'Заявка на добавление в контрагенты'
}
}
❌ GraphQL ERROR: {
errors: [ "Cannot read properties of undefined (reading 'receiverId')" ],
operationName: 'SendCounterpartyRequest',
timestamp: '2025-09-10T18:19:05.335Z'
}
POST /api/graphql 200 in 217ms
✓ Compiled /seller/settings in 395ms
GET /seller/settings 200 in 456ms
🔥 МОДУЛЬ SERVICES DOMAIN ЗАГРУЖАЕТСЯ
🔥 SERVICES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 INVENTORY DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER GOODS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 LOGISTICS CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 WILDBERRIES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ANALYTICS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 ADMIN TOOLS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 FILE MANAGEMENT DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 EXTERNAL ADS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🔥 SELLER CONSUMABLES DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:28:34.660Z',
variables: {}
}
POST /api/graphql 200 in 1677ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:28:35.343Z',
variables: {}
}
POST /api/graphql 200 in 459ms
GET /fulfillment/settings 200 in 70ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZiZ2gyd2wwMDAxeTVuYXAyNGZhc3VpIiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NzQwNzQzMCwiZXhwIjoxNzU5OTk5NDMwfQ.jw0t2qqwtuqBPbzJZ71iLim623iK4y8XCRtbByg8-Lw&orgId=cmfbghrno0002y5na8b59ykfr 200 in 5458680ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:28:52.227Z',
variables: {}
}
POST /api/graphql 200 in 545ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:28:52.750Z',
variables: {}
}
POST /api/graphql 200 in 472ms
GET /logistics/partners 200 in 126ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlODJtYnEwMDA4eTU2ZXlkM2N6d2FsIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NCIsImlhdCI6MTc1NzUyMzQzMSwiZXhwIjoxNzYwMTE1NDMxfQ.5hfVBi7WHoAkPssQupj3pnDMkCyo1YcvncLuJv10Tgo&orgId=cmfe838ja0009y56ed0rk8us5 200 in 611122ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlODJtYnEwMDA4eTU2ZXlkM2N6d2FsIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NCIsImlhdCI6MTc1NzUyMzQzMSwiZXhwIjoxNzYwMTE1NDMxfQ.5hfVBi7WHoAkPssQupj3pnDMkCyo1YcvncLuJv10Tgo&orgId=cmfe838ja0009y56ed0rk8us5 200 in 5478266ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 249ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:29:11.936Z',
variables: {}
}
POST /api/graphql 200 in 536ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T18:29:12.523Z',
variables: {}
}
📥 INCOMING_REQUESTS: {
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationId: 'cmfe838ja0009y56ed0rk8us5',
requestsCount: 0
}
POST /api/graphql 200 in 574ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyPartnerLink',
timestamp: '2025-09-10T18:29:13.079Z',
variables: {}
}
🔐 REFERRALS DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationId: 'cmfe838ja0009y56ed0rk8us5'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_PARTNER_LINK DOMAIN QUERY STARTED: { userId: 'cmfe82mbq0008y56eyd3czwal' }
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T18:29:13.306Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetOutgoingRequests',
timestamp: '2025-09-10T18:29:13.319Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T18:29:13.323Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:29:13.325Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyCounterparties',
timestamp: '2025-09-10T18:29:13.331Z',
variables: {}
}
✅ MY_PARTNER_LINK DOMAIN SUCCESS: {
link: 'http://localhost:3000/register?partner=FF_5010050095_1757523459573'
}
🎯 RESOLVER RESULT TYPE: string Has result
POST /api/graphql 200 in 581ms
📤 OUTGOING_REQUESTS: {
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationId: 'cmfe838ja0009y56ed0rk8us5',
requestsCount: 0
}
POST /api/graphql 200 in 1465ms
POST /api/graphql 200 in 1468ms
POST /api/graphql 200 in 1472ms
🤝 MY_COUNTERPARTIES: {
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationId: 'cmfe838ja0009y56ed0rk8us5',
organizationType: 'LOGIST',
counterpartiesCount: 0
}
POST /api/graphql 200 in 1473ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationType: 'LOGIST',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1923ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'SearchOrganizations',
timestamp: '2025-09-10T18:29:18.444Z',
variables: { type: 'SELLER', search: null }
}
POST /api/graphql 200 in 1186ms
GET /logistics/settings 200 in 67ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlODJtYnEwMDA4eTU2ZXlkM2N6d2FsIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NCIsImlhdCI6MTc1NzUyMzQzMSwiZXhwIjoxNzYwMTE1NDMxfQ.5hfVBi7WHoAkPssQupj3pnDMkCyo1YcvncLuJv10Tgo&orgId=cmfe838ja0009y56ed0rk8us5 200 in 35831ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:29:49.883Z',
variables: {}
}
POST /api/graphql 200 in 540ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:29:50.396Z',
variables: {}
}
POST /api/graphql 200 in 469ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlODJtYnEwMDA4eTU2ZXlkM2N6d2FsIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NCIsImlhdCI6MTc1NzUyMzQzMSwiZXhwIjoxNzYwMTE1NDMxfQ.5hfVBi7WHoAkPssQupj3pnDMkCyo1YcvncLuJv10Tgo&orgId=cmfe838ja0009y56ed0rk8us5 200 in 42313ms
GET /logistics/settings 200 in 143ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:29:56.095Z',
variables: {}
}
GET /favicon.ico?favicon.45db1c09.ico 200 in 247ms
POST /api/graphql 200 in 455ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T18:29:56.632Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:29:56.717Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T18:29:56.724Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T18:29:56.739Z',
variables: {}
}
📥 INCOMING_REQUESTS: {
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationId: 'cmfe838ja0009y56ed0rk8us5',
requestsCount: 0
}
POST /api/graphql 200 in 556ms
POST /api/graphql 200 in 562ms
POST /api/graphql 200 in 639ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfe82mbq0008y56eyd3czwal',
organizationType: 'LOGIST',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1147ms
GET /fulfillment/partners 200 in 121ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:30:33.318Z',
variables: {}
}
POST /api/graphql 200 in 568ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:30:33.902Z',
variables: {}
}
POST /api/graphql 200 in 496ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'SearchOrganizations',
timestamp: '2025-09-10T18:30:35.655Z',
variables: { type: 'FULFILLMENT', search: null }
}
POST /api/graphql 200 in 1196ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZiZ2gyd2wwMDAxeTVuYXAyNGZhc3VpIiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NzQwNzQzMCwiZXhwIjoxNzU5OTk5NDMwfQ.jw0t2qqwtuqBPbzJZ71iLim623iK4y8XCRtbByg8-Lw&orgId=cmfbghrno0002y5na8b59ykfr 200 in 77660ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZiZ2gyd2wwMDAxeTVuYXAyNGZhc3VpIiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NzQwNzQzMCwiZXhwIjoxNzU5OTk5NDMwfQ.jw0t2qqwtuqBPbzJZ71iLim623iK4y8XCRtbByg8-Lw&orgId=cmfbghrno0002y5na8b59ykfr 200 in 5638075ms
GET /fulfillment/partners 200 in 270ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:31:51.806Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T18:31:51.812Z'
}
POST /api/graphql 200 in 246ms
GET /favicon.ico?favicon.45db1c09.ico 200 in 251ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:31:52.036Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T18:31:52.036Z'
}
POST /api/graphql 200 in 175ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendSmsCode',
timestamp: '2025-09-10T18:31:57.924Z',
variables: { phone: '79999999999' }
}
POST /api/graphql 200 in 169ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:31:58.093Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T18:31:58.093Z'
}
POST /api/graphql 200 in 134ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifySmsCode',
timestamp: '2025-09-10T18:32:00.567Z',
variables: { phone: '79999999999', code: '1234' }
}
POST /api/graphql 200 in 809ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifyInn',
timestamp: '2025-09-10T18:33:07.226Z',
variables: { inn: '7838072550' }
}
🔍 VERIFY_INN STARTED: { inn: '7838072550' }
✅ VERIFY_INN: ИНН прошел валидацию, запрашиваем данные из DaData...
✅ VERIFY_INN SUCCESS: { inn: '7838072550', name: 'ФУЛФИЛМЕНТ ПЛАТФОРМА', isActive: true }
POST /api/graphql 200 in 798ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:09.327Z',
variables: {}
}
POST /api/graphql 200 in 249ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'RegisterFulfillmentOrganization',
timestamp: '2025-09-10T18:33:10.395Z',
variables: {
input: {
phone: '79999999999',
inn: '7838072550',
type: 'FULFILLMENT',
referralCode: null,
partnerCode: null
}
}
}
🏢 REGISTER_FULFILLMENT_ORGANIZATION - ВЫЗВАН: {
phone: '79999999999',
inn: '7838072550',
referralCode: null,
timestamp: '2025-09-10T18:33:10.396Z'
}
🔍 Получение данных организации из DaData для ИНН: 7838072550
✅ Данные из DaData получены: {
name: 'ФУЛФИЛМЕНТ ПЛАТФОРМА',
fullName: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ФУЛФИЛМЕНТ ПЛАТФОРМА"',
address: '198216, Г.САНКТ-ПЕТЕРБУРГ, ВН.ТЕР.Г. МУНИЦИПАЛЬНЫЙ ОКРУГ КНЯЖЕВО, ПР-КТ ТРАМВАЙНЫЙ, Д. 32, ЛИТЕРА А, ПОМЕЩ. 1-Н, ПОМЕЩ. 91',
isActive: true
}
✅ ФУЛФИЛМЕНТ ОРГАНИЗАЦИЯ СОЗДАНА: {
organizationId: 'cmfebi2qk000dy56ebbr0hsyy',
userId: 'cmfebgkpl000cy56el0zakya4',
inn: '7838072550',
type: 'FULFILLMENT',
referralCode: 'FF_7838072550_1757529190746'
}
POST /api/graphql 200 in 1660ms
GET /dashboard 200 in 142ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:14.431Z',
variables: {}
}
POST /api/graphql 200 in 461ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:14.956Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetIncomingRequests',
timestamp: '2025-09-10T18:33:15.050Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetPendingSuppliesCount',
timestamp: '2025-09-10T18:33:15.057Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetConversations',
timestamp: '2025-09-10T18:33:15.065Z',
variables: {}
}
POST /api/graphql 200 in 490ms
GET /fulfillment/home 200 in 79ms
📥 INCOMING_REQUESTS: {
userId: 'cmfebgkpl000cy56el0zakya4',
organizationId: 'cmfebi2qk000dy56ebbr0hsyy',
requestsCount: 0
}
POST /api/graphql 200 in 659ms
POST /api/graphql 200 in 683ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:15.551Z',
variables: {}
}
POST /api/graphql 200 in 431ms
📊 PENDING SUPPLIES COUNT: {
userId: 'cmfebgkpl000cy56el0zakya4',
organizationType: 'FULFILLMENT',
ourSupplyOrders: 0,
sellerSupplyOrders: 0,
incomingSupplierOrders: 0,
logisticsOrders: 0,
totalPending: 0
}
POST /api/graphql 200 in 1147ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:16.045Z',
variables: {}
}
POST /api/graphql 200 in 563ms
GET /fulfillment/partners 200 in 103ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:30.901Z',
variables: {}
}
POST /api/graphql 200 in 545ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:31.483Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyCounterparties',
timestamp: '2025-09-10T18:33:31.576Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetOutgoingRequests',
timestamp: '2025-09-10T18:33:31.583Z',
variables: {}
}
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMyPartnerLink',
timestamp: '2025-09-10T18:33:31.607Z',
variables: {}
}
🔐 REFERRALS DOMAIN AUTH CHECK: {
hasUser: true,
userId: 'cmfebgkpl000cy56el0zakya4',
organizationId: 'cmfebi2qk000dy56ebbr0hsyy'
}
✅ AUTH PASSED: Calling resolver
🔍 MY_PARTNER_LINK DOMAIN QUERY STARTED: { userId: 'cmfebgkpl000cy56el0zakya4' }
POST /api/graphql 200 in 506ms
🤝 MY_COUNTERPARTIES: {
userId: 'cmfebgkpl000cy56el0zakya4',
organizationId: 'cmfebi2qk000dy56ebbr0hsyy',
organizationType: 'FULFILLMENT',
counterpartiesCount: 0
}
POST /api/graphql 200 in 680ms
📤 OUTGOING_REQUESTS: {
userId: 'cmfebgkpl000cy56el0zakya4',
organizationId: 'cmfebi2qk000dy56ebbr0hsyy',
requestsCount: 0
}
POST /api/graphql 200 in 685ms
✅ MY_PARTNER_LINK DOMAIN SUCCESS: {
link: 'http://localhost:3000/register?partner=FF_7838072550_1757529190746'
}
🎯 RESOLVER RESULT TYPE: string Has result
POST /api/graphql 200 in 712ms
GET /seller/settings 200 in 139ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:39.781Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:39.782Z'
}
POST /api/graphql 200 in 150ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:39.927Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:39.927Z'
}
POST /api/graphql 200 in 121ms
🔍 RegisterContent - URL параметры: {
partnerCode: 'FF_7838072550_1757529190746',
referralCode: null,
searchParams: { partner: 'FF_7838072550_1757529190746' }
}
Недействительный партнерский код: FF_7838072550_1757529190746
GET /register?partner=FF_7838072550_1757529190746 200 in 144ms
GET /favicon.ico 200 in 66ms
SyntaxError: Unexpected end of JSON input
at JSON.parse (<anonymous>)
at new Promise (<anonymous>)
at new Promise (<anonymous>)
at POST (src/app/api/graphql/route.ts:162:9)
160 |
161 | export async function POST(request: NextRequest) {
> 162 | return handler(request)
| ^
163 | }
164 |
🔍 RegisterContent - URL параметры: { partnerCode: null, referralCode: null, searchParams: {} }
🚀 RegisterContent - Передача в AuthFlow: { partnerCode: null, referralCode: null }
GET /register 200 in 210ms
GET /favicon.ico 200 in 57ms
SyntaxError: Unexpected end of JSON input
at JSON.parse (<anonymous>)
at new Promise (<anonymous>)
at new Promise (<anonymous>)
at POST (src/app/api/graphql/route.ts:162:9)
160 |
161 | export async function POST(request: NextRequest) {
> 162 | return handler(request)
| ^
163 | }
164 |
GET /dashboard 200 in 151ms
GET /favicon.ico 200 in 54ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:45.670Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:45.671Z'
}
POST /api/graphql 200 in 179ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:45.840Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T18:33:45.840Z'
}
POST /api/graphql 200 in 130ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'SendSmsCode',
timestamp: '2025-09-10T18:34:28.621Z',
variables: { phone: '78888888888' }
}
POST /api/graphql 200 in 883ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:34:28.814Z',
variables: {}
}
❌ GraphQL ERROR: {
errors: [ 'Требуется авторизация' ],
operationName: 'GetMe',
timestamp: '2025-09-10T18:34:28.816Z'
}
POST /api/graphql 200 in 146ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'VerifySmsCode',
timestamp: '2025-09-10T18:34:31.356Z',
variables: { phone: '78888888888', code: '1234' }
}
POST /api/graphql 200 in 804ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'AddMarketplaceApiKey',
timestamp: '2025-09-10T18:34:45.478Z',
variables: {
input: {
marketplace: 'WILDBERRIES',
apiKey: 'eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjUwNTIwdjEiLCJ0eXAiOiJKV1QifQ.eyJlbnQiOjEsImV4cCI6MTc2NjgwODMzMiwiaWQiOiIwMTk3YjIyMy0xNDAwLTc1MTQtYjgwZS0wYzIyYjRiYWZlODYiLCJpaWQiOjM0MTU5NTU3LCJvaWQiOjI1MDAwOTkxMCwicyI6MTA3Mzc0OTc1OCwic2lkIjoiNTIwMmYwMzYtMWNiNC00MmIyLWFhODUtYWNiOWFjODJmYzYyIiwidCI6ZmFsc2UsInVpZCI6MzQxNTk1NTd9.tSjALISoFdkylX0iMuquXhpRJhTZwEMOZV_mfZh1wXUtRzyu4m8r5cvodaMVxcSNKifit2Vm2CWKw1Hp9z7_AQ',
validateOnly: true
}
}
}
🔍 ADD_MARKETPLACE_API_KEY DOMAIN MUTATION STARTED: { marketplace: 'WILDBERRIES', validateOnly: true, hasUser: true }
🔵 Starting Wildberries validation for key: eyJhbGciOiJFUzI1NiIs...
📡 Making ping request to: https://common-api.wildberries.ru/ping
📡 Ping response: { status: 200, data: { TS: '2025-09-10T18:34:45Z', Status: 'OK' } }
POST /api/graphql 200 in 996ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'AddMarketplaceApiKey',
timestamp: '2025-09-10T18:34:47.753Z',
variables: {
input: {
marketplace: 'WILDBERRIES',
apiKey: 'eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjUwNTIwdjEiLCJ0eXAiOiJKV1QifQ.eyJlbnQiOjEsImV4cCI6MTc2NjgwODMzMiwiaWQiOiIwMTk3YjIyMy0xNDAwLTc1MTQtYjgwZS0wYzIyYjRiYWZlODYiLCJpaWQiOjM0MTU5NTU3LCJvaWQiOjI1MDAwOTkxMCwicyI6MTA3Mzc0OTc1OCwic2lkIjoiNTIwMmYwMzYtMWNiNC00MmIyLWFhODUtYWNiOWFjODJmYzYyIiwidCI6ZmFsc2UsInVpZCI6MzQxNTk1NTd9.tSjALISoFdkylX0iMuquXhpRJhTZwEMOZV_mfZh1wXUtRzyu4m8r5cvodaMVxcSNKifit2Vm2CWKw1Hp9z7_AQ',
validateOnly: true
}
}
}
🔍 ADD_MARKETPLACE_API_KEY DOMAIN MUTATION STARTED: { marketplace: 'WILDBERRIES', validateOnly: true, hasUser: true }
🔵 Starting Wildberries validation for key: eyJhbGciOiJFUzI1NiIs...
📡 Making ping request to: https://common-api.wildberries.ru/ping
📡 Ping response: { status: 200, data: { TS: '2025-09-10T18:34:47Z', Status: 'OK' } }
POST /api/graphql 200 in 390ms
🌐 GraphQL REQUEST: {
operationType: 'query',
operationName: 'GetMe',
timestamp: '2025-09-10T18:34:48.250Z',
variables: {}
}
POST /api/graphql 200 in 212ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'RegisterSellerOrganization',
timestamp: '2025-09-10T18:34:49.832Z',
variables: {
input: {
phone: '78888888888',
wbApiKey: 'eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjUwNTIwdjEiLCJ0eXAiOiJKV1QifQ.eyJlbnQiOjEsImV4cCI6MTc2NjgwODMzMiwiaWQiOiIwMTk3YjIyMy0xNDAwLTc1MTQtYjgwZS0wYzIyYjRiYWZlODYiLCJpaWQiOjM0MTU5NTU3LCJvaWQiOjI1MDAwOTkxMCwicyI6MTA3Mzc0OTc1OCwic2lkIjoiNTIwMmYwMzYtMWNiNC00MmIyLWFhODUtYWNiOWFjODJmYzYyIiwidCI6ZmFsc2UsInVpZCI6MzQxNTk1NTd9.tSjALISoFdkylX0iMuquXhpRJhTZwEMOZV_mfZh1wXUtRzyu4m8r5cvodaMVxcSNKifit2Vm2CWKw1Hp9z7_AQ',
referralCode: null,
partnerCode: null
}
}
}
🛍️ REGISTER_SELLER_ORGANIZATION - ВЫЗВАН: {
phone: '78888888888',
inn: undefined,
fulfillmentPartnerId: undefined,
referralCode: null,
timestamp: '2025-09-10T18:34:49.833Z'
}
POST /api/graphql 200 in 320ms
🌐 GraphQL REQUEST: {
operationType: 'mutation',
operationName: 'RegisterSellerOrganization',
timestamp: '2025-09-10T18:34:57.225Z',
variables: {
input: {
phone: '78888888888',
wbApiKey: 'eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjUwNTIwdjEiLCJ0eXAiOiJKV1QifQ.eyJlbnQiOjEsImV4cCI6MTc2NjgwODMzMiwiaWQiOiIwMTk3YjIyMy0xNDAwLTc1MTQtYjgwZS0wYzIyYjRiYWZlODYiLCJpaWQiOjM0MTU5NTU3LCJvaWQiOjI1MDAwOTkxMCwicyI6MTA3Mzc0OTc1OCwic2lkIjoiNTIwMmYwMzYtMWNiNC00MmIyLWFhODUtYWNiOWFjODJmYzYyIiwidCI6ZmFsc2UsInVpZCI6MzQxNTk1NTd9.tSjALISoFdkylX0iMuquXhpRJhTZwEMOZV_mfZh1wXUtRzyu4m8r5cvodaMVxcSNKifit2Vm2CWKw1Hp9z7_AQ',
referralCode: null,
partnerCode: null
}
}
}
🛍️ REGISTER_SELLER_ORGANIZATION - ВЫЗВАН: {
phone: '78888888888',
inn: undefined,
fulfillmentPartnerId: undefined,
referralCode: null,
timestamp: '2025-09-10T18:34:57.226Z'
}
POST /api/graphql 200 in 241ms
[Error: ENOENT: no such file or directory, open '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/app/dashboard/page/app-build-manifest.json'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/app/dashboard/page/app-build-manifest.json'
}
○ Compiling /_error ...
✓ Compiled /_error in 945ms
[Error: ENOENT: no such file or directory, open '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/static/development/_buildManifest.js.tmp.dlx4f6op8wr'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/static/development/_buildManifest.js.tmp.dlx4f6op8wr'
}
[Error: ENOENT: no such file or directory, open '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/app/dashboard/page/app-build-manifest.json'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/app/dashboard/page/app-build-manifest.json'
}
[Error: ENOENT: no such file or directory, open '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/static/development/_buildManifest.js.tmp.6h7o7t0wjf3'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/static/development/_buildManifest.js.tmp.6h7o7t0wjf3'
}
[Error: ENOENT: no such file or directory, open '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/static/development/_buildManifest.js.tmp.qtff5akybsf'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/static/development/_buildManifest.js.tmp.qtff5akybsf'
}
[Error: ENOENT: no such file or directory, open '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/app/favicon.ico/[__metadata_id__]/route/app-paths-manifest.json'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/app/favicon.ico/[__metadata_id__]/route/app-paths-manifest.json'
}
[Error: ENOENT: no such file or directory, open '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/app/dashboard/page/app-build-manifest.json'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/app/dashboard/page/app-build-manifest.json'
}
Error: Cannot find module '../chunks/ssr/[turbopack]_runtime.js'
Require stack:
- /Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/pages/_document.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/server/require.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/server/load-components.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/build/utils.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/build/swc/options.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/build/swc/index.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/build/next-config-ts/transpile-config.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/server/config.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/server/next.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/server/lib/start-server.js
at Object.<anonymous> (.next/server/pages/_document.js:2:17) {
code: 'MODULE_NOT_FOUND',
requireStack: [Array]
}
GET /dashboard 500 in 299ms
[Error: ENOENT: no such file or directory, open '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/app/page/app-build-manifest.json'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/app/page/app-build-manifest.json'
}
Error: Cannot find module '../chunks/ssr/[turbopack]_runtime.js'
Require stack:
- /Users/veronikasmirnova/Desktop/Projects/sfera/.next/server/pages/_document.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/server/require.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/server/load-components.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/build/utils.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/build/swc/options.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/build/swc/index.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/build/next-config-ts/transpile-config.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/server/config.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/server/next.js
- /Users/veronikasmirnova/Desktop/Projects/sfera/node_modules/next/dist/server/lib/start-server.js
at Object.<anonymous> (.next/server/pages/_document.js:2:17) {
code: 'MODULE_NOT_FOUND',
requireStack: [Array]
}
GET / 500 in 149ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlYmdrcGwwMDBjeTU2ZWwwemFreWE0IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NzUyOTEyMSwiZXhwIjoxNzYwMTIxMTIxfQ.fGeTkTM-3ElCZxaUbsPj6QJ-4E_9xEn0IepdFV-asQ0&orgId=cmfebi2qk000dy56ebbr0hsyy 200 in 998063ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlODJtYnEwMDA4eTU2ZXlkM2N6d2FsIiwicGhvbmUiOiI3Nzc3Nzc3Nzc3NCIsImlhdCI6MTc1NzUyMzQzMSwiZXhwIjoxNzYwMTE1NDMxfQ.5hfVBi7WHoAkPssQupj3pnDMkCyo1YcvncLuJv10Tgo&orgId=cmfe838ja0009y56ed0rk8us5 200 in 1196373ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlYXFiaHkwMDBheTU2ZTBkaHNvNmY5IiwicGhvbmUiOiI3NjY2NjY2NjY2NiIsImlhdCI6MTc1NzUyNzg5NiwiZXhwIjoxNzYwMTE5ODk2fQ.7cwydrwVtytu-pSUrH6KnmIqI0tyPEsYuhaYzfithFY&orgId=cmfear815000by56e454o1xs6 200 in 2251005ms
GET /api/events?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWZlYmdrcGwwMDBjeTU2ZWwwemFreWE0IiwicGhvbmUiOiI3OTk5OTk5OTk5OSIsImlhdCI6MTc1NzUyOTEyMSwiZXhwIjoxNzYwMTIxMTIxfQ.fGeTkTM-3ElCZxaUbsPj6QJ-4E_9xEn0IepdFV-asQ0&orgId=cmfebi2qk000dy56ebbr0hsyy 200 in 981555ms
[?25h

View File

@ -1,10272 +0,0 @@
/**
* LEGACY MONOLITHIC FILE - НЕ ИСПОЛЬЗУЕТСЯ
*
* ⚠️ ВАЖНО: Этот файл больше НЕ используется в системе!
*
* Все резолверы мигрированы в доменную архитектуру:
* - src/graphql/resolvers/domains/ - 22 доменных модуля
* - src/graphql/resolvers/index.ts - новая точка входа
*
* Файл сохранен только для справки и истории.
* В production используется только модульная система.
*
* Статус миграции: ✅ 100% завершено
* Дата завершения: 10.09.2025
*/
import { Prisma } from '@prisma/client'
import bcrypt from 'bcryptjs'
import { GraphQLError, GraphQLScalarType, Kind } from 'graphql'
import jwt from 'jsonwebtoken'
import { prisma } from '@/lib/prisma'
import { notifyMany, notifyOrganization } from '@/lib/realtime'
import { DaDataService } from '@/services/dadata-service'
import { MarketplaceService } from '@/services/marketplace-service'
import { SmsService } from '@/services/sms-service'
import { WildberriesService } from '@/services/wildberries-service'
import { fulfillmentConsumableV2Queries as fulfillmentConsumableV2QueriesRestored, fulfillmentConsumableV2Mutations as fulfillmentConsumableV2MutationsRestored } from './resolvers/fulfillment-consumables-v2-restored'
import { fulfillmentInventoryV2Queries } from './resolvers/fulfillment-inventory-v2'
import { fulfillmentServicesQueries, fulfillmentServicesMutations } from './resolvers/fulfillment-services-v2'
import { sellerGoodsQueries, sellerGoodsMutations } from './resolvers/goods-supply-v2'
import { logisticsConsumableV2Queries, logisticsConsumableV2Mutations } from './resolvers/logistics-consumables-v2'
import { sellerConsumableQueries, sellerConsumableMutations } from './resolvers/seller-consumables'
import { sellerInventoryV2Queries } from './resolvers/seller-inventory-v2'
import { CommercialDataAudit } from './security/commercial-data-audit'
import { createSecurityContext } from './security/index'
import '@/lib/seed-init' // Автоматическая инициализация БД
// 🔒 HELPER: Создание безопасного контекста с организационными данными
function createSecureContextWithOrgData(context: Context, currentUser: { organization: { id: string; type: string } }) {
return {
...context,
user: {
...context.user,
organizationType: currentUser.organization.type,
organizationId: currentUser.organization.id,
},
}
}
import { ParticipantIsolation } from './security/participant-isolation'
import { SupplyDataFilter } from './security/supply-data-filter'
import type { SecurityContext } from './security/types'
// Сервисы
const smsService = new SmsService()
const dadataService = new DaDataService()
const marketplaceService = new MarketplaceService()
// Функция генерации уникального реферального кода
const generateReferralCode = async (): Promise<string> => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
let attempts = 0
const maxAttempts = 10
while (attempts < maxAttempts) {
let code = ''
for (let i = 0; i < 10; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length))
}
// Проверяем уникальность
const existing = await prisma.organization.findUnique({
where: { referralCode: code },
})
if (!existing) {
return code
}
attempts++
}
// Если не удалось сгенерировать уникальный код, используем cuid как fallback
return `REF${Date.now()}${Math.random().toString(36).substr(2, 5).toUpperCase()}`
}
// Функция для автоматического создания записи склада при новом партнерстве
const autoCreateWarehouseEntry = async (sellerId: string, fulfillmentId: string) => {
console.warn(`🏗️ AUTO WAREHOUSE ENTRY: Creating for seller ${sellerId} with fulfillment ${fulfillmentId}`)
// Получаем данные селлера
const sellerOrg = await prisma.organization.findUnique({
where: { id: sellerId },
})
if (!sellerOrg) {
throw new Error(`Селлер с ID ${sellerId} не найден`)
}
// Проверяем что не существует уже записи для этого селлера у этого фулфилмента
// В будущем здесь может быть проверка в отдельной таблице warehouse_entries
// Пока используем логику проверки через контрагентов
// ЛОГИКА ОПРЕДЕЛЕНИЯ НАЗВАНИЯ МАГАЗИНА (консистентно с warehouseData resolver)
let storeName = sellerOrg.name
if (sellerOrg.fullName && sellerOrg.name?.includes('ИП')) {
// Извлекаем название из скобок, например: "ИП Антипова Д. В. (Renrel)" -> "Renrel"
const match = sellerOrg.fullName.match(/\(([^)]+)\)/)
if (match && match[1]) {
storeName = match[1]
}
}
// Создаем структуру данных для склада
const warehouseEntry = {
id: `warehouse_${sellerId}_${Date.now()}`, // Уникальный ID записи
storeName: storeName || sellerOrg.fullName || sellerOrg.name,
storeOwner: sellerOrg.inn || sellerOrg.fullName || sellerOrg.name,
storeImage: sellerOrg.logoUrl || null,
storeQuantity: 0, // Пока нет поставок
partnershipDate: new Date(),
products: [], // Пустой массив продуктов
}
console.warn('✅ AUTO WAREHOUSE ENTRY CREATED:', {
sellerId,
storeName: warehouseEntry.storeName,
storeOwner: warehouseEntry.storeOwner,
})
// В реальной системе здесь бы была запись в таблицу warehouse_entries
// Пока возвращаем структуру данных
return warehouseEntry
}
// Интерфейсы для типизации
interface Context {
user?: {
id: string
phone: string
}
admin?: {
id: string
username: string
}
}
interface CreateEmployeeInput {
firstName: string
lastName: string
middleName?: string
birthDate?: string
avatar?: string
passportPhoto?: string
passportSeries?: string
passportNumber?: string
passportIssued?: string
passportDate?: string
address?: string
position: string
department?: string
hireDate: string
salary?: number
phone: string
email?: string
telegram?: string
whatsapp?: string
emergencyContact?: string
emergencyPhone?: string
}
interface UpdateEmployeeInput {
firstName?: string
lastName?: string
middleName?: string
birthDate?: string
avatar?: string
passportPhoto?: string
passportSeries?: string
passportNumber?: string
passportIssued?: string
passportDate?: string
address?: string
position?: string
department?: string
hireDate?: string
salary?: number
status?: 'ACTIVE' | 'VACATION' | 'SICK' | 'FIRED'
phone?: string
email?: string
telegram?: string
whatsapp?: string
emergencyContact?: string
emergencyPhone?: string
}
interface UpdateScheduleInput {
employeeId: string
date: string
status: 'WORK' | 'WEEKEND' | 'VACATION' | 'SICK' | 'ABSENT'
hoursWorked?: number
overtimeHours?: number
notes?: string
}
interface AuthTokenPayload {
userId: string
phone: string
}
// JWT утилиты
const generateToken = (payload: AuthTokenPayload): string => {
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '30d' })
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const verifyToken = (token: string): AuthTokenPayload => {
try {
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
throw new GraphQLError('Недействительный токен', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
}
// Скалярный тип для JSON
const JSONScalar = new GraphQLScalarType({
name: 'JSON',
description: 'JSON custom scalar type',
serialize(value: unknown) {
return value // значение отправляется клиенту
},
parseValue(value: unknown) {
return value // значение получено от клиента
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value)
case Kind.OBJECT: {
const value = Object.create(null)
ast.fields.forEach((field) => {
value[field.name.value] = parseLiteral(field.value)
})
return value
}
case Kind.LIST:
return ast.values.map(parseLiteral)
default:
return null
}
},
})
// Скалярный тип для DateTime
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'DateTime custom scalar type',
serialize(value: unknown) {
if (value instanceof Date) {
return value.toISOString() // значение отправляется клиенту как ISO строка
}
return value
},
parseValue(value: unknown) {
if (typeof value === 'string') {
return new Date(value) // значение получено от клиента, парсим как дату
}
return value
},
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
return new Date(ast.value) // AST значение как дата
}
return null
},
})
function parseLiteral(ast: unknown): unknown {
const astNode = ast as {
kind: string
value?: unknown
fields?: unknown[]
values?: unknown[]
}
switch (astNode.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return astNode.value
case Kind.INT:
case Kind.FLOAT:
return parseFloat(astNode.value as string)
case Kind.OBJECT: {
const value = Object.create(null)
if (astNode.fields) {
astNode.fields.forEach((field: unknown) => {
const fieldNode = field as {
name: { value: string }
value: unknown
}
value[fieldNode.name.value] = parseLiteral(fieldNode.value)
})
}
return value
}
case Kind.LIST:
return (ast as { values: unknown[] }).values.map(parseLiteral)
default:
return null
}
}
export const resolvers = {
JSON: JSONScalar,
DateTime: DateTimeScalar,
Query: {
me: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
return await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
},
organization: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const organization = await prisma.organization.findUnique({
where: { id: args.id },
include: {
apiKeys: true,
users: true,
},
})
if (!organization) {
throw new GraphQLError('Организация не найдена')
}
// Проверяем, что пользователь имеет доступ к этой организации
const hasAccess = organization.users.some((user) => user.id === context.user!.id)
if (!hasAccess) {
throw new GraphQLError('Нет доступа к этой организации', {
extensions: { code: 'FORBIDDEN' },
})
}
return organization
},
// Поиск организаций по типу для добавления в контрагенты
searchOrganizations: async (_: unknown, args: { type?: string; search?: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
// Получаем текущую организацию пользователя
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Получаем уже существующих контрагентов для добавления флага
const existingCounterparties = await prisma.counterparty.findMany({
where: { organizationId: currentUser.organization.id },
select: { counterpartyId: true },
})
const existingCounterpartyIds = existingCounterparties.map((c) => c.counterpartyId)
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
const outgoingRequests = await prisma.counterpartyRequest.findMany({
where: {
senderId: currentUser.organization.id,
status: 'PENDING',
},
select: { receiverId: true },
})
const outgoingRequestIds = outgoingRequests.map((r) => r.receiverId)
// Получаем входящие заявки для добавления флага hasIncomingRequest
const incomingRequests = await prisma.counterpartyRequest.findMany({
where: {
receiverId: currentUser.organization.id,
status: 'PENDING',
},
select: { senderId: true },
})
const incomingRequestIds = incomingRequests.map((r) => r.senderId)
const where: Record<string, unknown> = {
// Больше не исключаем собственную организацию
}
if (args.type) {
where.type = args.type
}
if (args.search) {
where.OR = [
{ name: { contains: args.search, mode: 'insensitive' } },
{ fullName: { contains: args.search, mode: 'insensitive' } },
{ inn: { contains: args.search } },
]
}
const organizations = await prisma.organization.findMany({
where,
take: 50, // Ограничиваем количество результатов
orderBy: { createdAt: 'desc' },
include: {
users: true,
apiKeys: true,
},
})
// Добавляем флаги isCounterparty, isCurrentUser, hasOutgoingRequest и hasIncomingRequest к каждой организации
return organizations.map((org) => ({
...org,
isCounterparty: existingCounterpartyIds.includes(org.id),
isCurrentUser: org.id === currentUser.organization?.id,
hasOutgoingRequest: outgoingRequestIds.includes(org.id),
hasIncomingRequest: incomingRequestIds.includes(org.id),
}))
},
// Мои контрагенты
myCounterparties: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
const counterparties = await prisma.counterparty.findMany({
where: { organizationId: currentUser.organization.id },
include: {
counterparty: {
include: {
users: true,
apiKeys: true,
},
},
},
})
return counterparties.map((c) => c.counterparty)
},
// Поставщики поставок
supplySuppliers: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
const suppliers = await prisma.supplySupplier.findMany({
where: { organizationId: currentUser.organization.id },
orderBy: { createdAt: 'desc' },
})
return suppliers
},
// Логистика конкретной организации
organizationLogistics: async (_: unknown, args: { organizationId: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
return await prisma.logistics.findMany({
where: { organizationId: args.organizationId },
orderBy: { createdAt: 'desc' },
})
},
// Входящие заявки
incomingRequests: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
return await prisma.counterpartyRequest.findMany({
where: {
receiverId: currentUser.organization.id,
status: 'PENDING',
},
include: {
sender: {
include: {
users: true,
apiKeys: true,
},
},
receiver: {
include: {
users: true,
apiKeys: true,
},
},
},
orderBy: { createdAt: 'desc' },
})
},
// Исходящие заявки
outgoingRequests: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
return await prisma.counterpartyRequest.findMany({
where: {
senderId: currentUser.organization.id,
status: { in: ['PENDING', 'REJECTED'] },
},
include: {
sender: {
include: {
users: true,
apiKeys: true,
},
},
receiver: {
include: {
users: true,
apiKeys: true,
},
},
},
orderBy: { createdAt: 'desc' },
})
},
// Сообщения с контрагентом
messages: async (
_: unknown,
args: { counterpartyId: string; limit?: number; offset?: number },
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
const limit = args.limit || 50
const offset = args.offset || 0
const messages = await prisma.message.findMany({
where: {
OR: [
{
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.counterpartyId,
},
{
senderOrganizationId: args.counterpartyId,
receiverOrganizationId: currentUser.organization.id,
},
],
},
include: {
sender: true,
senderOrganization: {
include: {
users: true,
},
},
receiverOrganization: {
include: {
users: true,
},
},
},
orderBy: { createdAt: 'asc' },
take: limit,
skip: offset,
})
return messages
},
// Список чатов (последние сообщения с каждым контрагентом)
conversations: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Получаем всех контрагентов
const counterparties = await prisma.counterparty.findMany({
where: { organizationId: currentUser.organization.id },
include: {
counterparty: {
include: {
users: true,
},
},
},
})
// Для каждого контрагента получаем последнее сообщение и количество непрочитанных
const conversations = await Promise.all(
counterparties.map(async (cp) => {
const counterpartyId = cp.counterparty.id
// Последнее сообщение с этим контрагентом
const lastMessage = await prisma.message.findFirst({
where: {
OR: [
{
senderOrganizationId: currentUser.organization!.id,
receiverOrganizationId: counterpartyId,
},
{
senderOrganizationId: counterpartyId,
receiverOrganizationId: currentUser.organization!.id,
},
],
},
include: {
sender: true,
senderOrganization: {
include: {
users: true,
},
},
receiverOrganization: {
include: {
users: true,
},
},
},
orderBy: { createdAt: 'desc' },
})
// Количество непрочитанных сообщений от этого контрагента
const unreadCount = await prisma.message.count({
where: {
senderOrganizationId: counterpartyId,
receiverOrganizationId: currentUser.organization!.id,
isRead: false,
},
})
// Если есть сообщения с этим контрагентом, включаем его в список
if (lastMessage) {
return {
id: `${currentUser.organization!.id}-${counterpartyId}`,
counterparty: cp.counterparty,
lastMessage,
unreadCount,
updatedAt: lastMessage.createdAt,
}
}
return null
}),
)
// Фильтруем null значения и сортируем по времени последнего сообщения
return conversations
.filter((conv) => conv !== null)
.sort((a, b) => new Date(b!.updatedAt).getTime() - new Date(a!.updatedAt).getTime())
},
// Мои услуги
myServices: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Услуги доступны только для фулфилмент центров')
}
return await prisma.service.findMany({
where: { organizationId: currentUser.organization.id },
include: { organization: true },
orderBy: { createdAt: 'desc' },
})
},
// Расходники селлеров (материалы клиентов на складе фулфилмента)
mySupplies: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
return [] // Только фулфилменты имеют расходники
}
// Получаем расходники из V2 инвентаря фулфилмента
const inventoryItems = await prisma.fulfillmentConsumableInventory.findMany({
where: { fulfillmentCenterId: currentUser.organization.id },
include: {
product: true,
fulfillmentCenter: true,
},
orderBy: { lastSupplyDate: 'desc' },
})
// Преобразуем V2 структуру в формат для services/supplies
const transformedSupplies = inventoryItems.map((item) => ({
id: item.id,
name: item.product.name,
description: item.product.description || '',
pricePerUnit: item.resalePrice ? parseFloat(item.resalePrice.toString()) : null, // Цена перепродажи
unit: 'шт', // TODO: добавить unit в Product модель
imageUrl: item.product.mainImage,
warehouseStock: item.currentStock, // Текущий остаток V2
isAvailable: item.currentStock > 0, // Есть ли в наличии
warehouseConsumableId: item.id, // ID из V2 инвентаря
createdAt: item.createdAt,
updatedAt: item.updatedAt,
organization: item.fulfillmentCenter,
}))
console.warn('🔥 SUPPLIES RESOLVER - NEW FORMAT:', {
organizationId: currentUser.organization.id,
suppliesCount: transformedSupplies.length,
supplies: transformedSupplies.map((s) => ({
id: s.id,
name: s.name,
pricePerUnit: s.pricePerUnit,
warehouseStock: s.warehouseStock,
isAvailable: s.isAvailable,
})),
})
return transformedSupplies
},
// Доступные расходники для рецептур селлеров (только с ценой и в наличии)
getAvailableSuppliesForRecipe: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Селлеры могут получать расходники от своих фулфилмент-партнеров
if (currentUser.organization.type !== 'SELLER') {
return [] // Только селлеры используют рецептуры
}
// TODO: В будущем здесь будет логика получения расходников от партнерских фулфилментов
// Пока возвращаем пустой массив, так как эта функциональность еще разрабатывается
console.warn('🔥 getAvailableSuppliesForRecipe called for seller:', {
sellerId: currentUser.organization.id,
sellerName: currentUser.organization.name,
})
return []
},
// ЗАКОММЕНТИРОВАНО: Старый resolver заменен на V2 из fulfillment-inventory-v2.ts
/*
myFulfillmentSupplies: async (_: unknown, __: unknown, context: Context) => {
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥')
if (!context.user) {
console.warn('❌ No user in context')
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
console.warn('👤 Current user:', {
id: currentUser?.id,
phone: currentUser?.phone,
organizationId: currentUser?.organizationId,
organizationType: currentUser?.organization?.type,
organizationName: currentUser?.organization?.name,
})
if (!currentUser?.organization) {
console.warn('❌ No organization for user')
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
console.warn('❌ User organization is not FULFILLMENT:', currentUser.organization.type)
throw new GraphQLError('Доступ только для фулфилмент центров')
}
// Получаем расходники фулфилмента из V2 таблицы FulfillmentConsumableInventory
const inventoryItems = await prisma.fulfillmentConsumableInventory.findMany({
where: {
fulfillmentCenterId: currentUser.organization.id,
},
include: {
product: true,
fulfillmentCenter: true,
},
orderBy: { lastSupplyDate: 'desc' },
})
// Логирование для отладки
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (V2 ARCHITECTURE) 🔥🔥🔥')
console.warn('📊 Расходники фулфилмента из V2 инвентаря:', {
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
inventoryItemsCount: inventoryItems.length,
inventoryItems: inventoryItems.map((item) => ({
id: item.id,
productName: item.product.name,
currentStock: item.currentStock,
totalReceived: item.totalReceived,
totalShipped: item.totalShipped,
})),
})
// Преобразуем V2 инвентарь в V1 формат для совместимости с фронтом
return inventoryItems.map((item) => ({
// === ОСНОВНЫЕ ПОЛЯ ===
id: item.id,
name: item.product.name,
article: item.product.article,
description: item.product.description || '',
category: item.product.category?.name || 'Расходники',
unit: 'шт', // TODO: добавить в Product модель
// === СКЛАДСКИЕ ДАННЫЕ ===
currentStock: item.currentStock,
minStock: item.minStock,
usedStock: item.totalShipped, // V2: всего отгружено = использовано
quantity: item.totalReceived, // V2: всего получено = количество
shippedQuantity: item.totalShipped, // Для совместимости
// === ЦЕНЫ ===
price: parseFloat(item.averageCost.toString()),
// === СТАТУС ===
status: item.currentStock > 0 ? 'in-stock' : 'out-of-stock',
// === ДАТЫ ===
date: item.lastSupplyDate?.toISOString() || item.createdAt.toISOString(),
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
// === ПОСТАВЩИК ===
supplier: 'Различные поставщики', // V2 инвентарь агрегирует данные от разных поставщиков
// === ДОПОЛНИТЕЛЬНО ===
imageUrl: item.product.mainImage,
type: 'FULFILLMENT_CONSUMABLES', // Для совместимости
organizationId: item.fulfillmentCenterId,
}))
},
*/
// Заказы поставок расходников
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('У пользователя нет организации')
}
console.warn('🔍 SUPPLY ORDERS RESOLVER:', {
userId: context.user.id,
organizationType: currentUser.organization.type,
organizationId: currentUser.organization.id,
organizationName: currentUser.organization.name,
})
try {
// Возвращаем заказы где текущая организация является заказчиком, поставщиком, получателем или логистическим партнером
const orders = await prisma.supplyOrder.findMany({
where: {
OR: [
{ organizationId: currentUser.organization.id }, // Заказы созданные организацией
{ partnerId: currentUser.organization.id }, // Заказы где организация - поставщик
{ fulfillmentCenterId: currentUser.organization.id }, // Заказы где организация - получатель (фулфилмент)
{ logisticsPartnerId: currentUser.organization.id }, // Заказы где организация - логистический партнер
],
},
include: {
partner: true,
organization: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
},
orderBy: { createdAt: 'desc' },
})
console.warn('📦 SUPPLY ORDERS FOUND:', {
totalOrders: orders.length,
ordersByRole: {
asCreator: orders.filter((o) => o.organizationId === currentUser.organization.id).length,
asPartner: orders.filter((o) => o.partnerId === currentUser.organization.id).length,
asFulfillment: orders.filter((o) => o.fulfillmentCenterId === currentUser.organization.id).length,
asLogistics: orders.filter((o) => o.logisticsPartnerId === currentUser.organization.id).length,
},
orderStatuses: orders.reduce((acc: any, order) => {
acc[order.status] = (acc[order.status] || 0) + 1
return acc
}, {}),
orderIds: orders.map((o) => o.id),
})
return orders
} catch (error) {
console.error('❌ ERROR IN SUPPLY ORDERS RESOLVER:', error)
throw new GraphQLError(`Ошибка получения заказов поставок: ${error}`)
}
},
// Счетчик поставок, требующих одобрения
pendingSuppliesCount: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Считаем заказы поставок, требующие действий
// Расходники фулфилмента (созданные нами для себя) - требуют действий по статусам
const ourSupplyOrders = await prisma.supplyOrder.count({
where: {
organizationId: currentUser.organization.id, // Создали мы
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
status: { in: ['CONFIRMED', 'IN_TRANSIT'] }, // Подтверждено или в пути
},
})
// Расходники селлеров (созданные другими для нас) - требуют действий фулфилмента
const sellerSupplyOrders = await prisma.supplyOrder.count({
where: {
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
organizationId: { not: currentUser.organization.id }, // Создали НЕ мы
status: {
in: [
'SUPPLIER_APPROVED', // Поставщик подтвердил - нужно назначить логистику
'IN_TRANSIT', // В пути - нужно подтвердить получение
],
},
},
})
// 🔔 ВХОДЯЩИЕ ЗАКАЗЫ ДЛЯ ПОСТАВЩИКОВ (WHOLESALE) - требуют подтверждения
const incomingSupplierOrders = await prisma.supplyOrder.count({
where: {
partnerId: currentUser.organization.id, // Мы - поставщик
status: 'PENDING', // Ожидает подтверждения от поставщика
},
})
// 🚚 ЛОГИСТИЧЕСКИЕ ЗАЯВКИ ДЛЯ ЛОГИСТИКИ (LOGIST) - требуют действий логистики
const logisticsOrders = await prisma.supplyOrder.count({
where: {
logisticsPartnerId: currentUser.organization.id, // Мы - назначенная логистика
status: {
in: [
'CONFIRMED', // Подтверждено фулфилментом - нужно подтвердить логистикой
'LOGISTICS_CONFIRMED', // Подтверждено логистикой - нужно забрать товар у поставщика
],
},
},
})
// Общий счетчик поставок в зависимости от типа организации
let pendingSupplyOrders = 0
if (currentUser.organization.type === 'FULFILLMENT') {
pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders
} else if (currentUser.organization.type === 'WHOLESALE') {
pendingSupplyOrders = incomingSupplierOrders
} else if (currentUser.organization.type === 'LOGIST') {
pendingSupplyOrders = logisticsOrders
} else if (currentUser.organization.type === 'SELLER') {
pendingSupplyOrders = 0 // Селлеры не подтверждают поставки, только отслеживают
}
// Считаем входящие заявки на партнерство со статусом PENDING
const pendingIncomingRequests = await prisma.counterpartyRequest.count({
where: {
receiverId: currentUser.organization.id,
status: 'PENDING',
},
})
return {
supplyOrders: pendingSupplyOrders,
ourSupplyOrders: ourSupplyOrders, // Расходники фулфилмента
sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров
incomingSupplierOrders: incomingSupplierOrders, // 🔔 Входящие заказы для поставщиков
logisticsOrders: logisticsOrders, // 🚚 Логистические заявки для логистики
incomingRequests: pendingIncomingRequests,
total: pendingSupplyOrders + pendingIncomingRequests,
}
},
// Статистика склада фулфилмента с изменениями за сутки
fulfillmentWarehouseStats: async (_: unknown, __: unknown, context: Context) => {
console.warn('🔥 FULFILLMENT WAREHOUSE STATS RESOLVER CALLED')
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступ разрешен только для фулфилмент-центров')
}
const organizationId = currentUser.organization.id
// Получаем дату начала суток (24 часа назад)
const oneDayAgo = new Date()
oneDayAgo.setDate(oneDayAgo.getDate() - 1)
console.warn(`🏢 Organization ID: ${organizationId}, Date 24h ago: ${oneDayAgo.toISOString()}`)
// Сначала проверим ВСЕ заказы поставок
const allSupplyOrders = await prisma.supplyOrder.findMany({
where: { status: 'DELIVERED' },
include: {
items: {
include: { product: true },
},
organization: { select: { id: true, name: true, type: true } },
},
})
console.warn(`📦 ALL DELIVERED ORDERS: ${allSupplyOrders.length}`)
allSupplyOrders.forEach((order) => {
console.warn(
` Order ${order.id}: org=${order.organizationId} (${order.organization?.name}), fulfillment=${order.fulfillmentCenterId}, items=${order.items.length}`,
)
})
// Продукты (товары от селлеров) - заказы К нам, но исключаем расходники фулфилмента
const sellerDeliveredOrders = await prisma.supplyOrder.findMany({
where: {
fulfillmentCenterId: organizationId, // Доставлено к нам (фулфилменту)
organizationId: { not: organizationId }, // ИСПРАВЛЕНО: исключаем заказы самого фулфилмента
status: 'DELIVERED',
},
include: {
items: {
include: { product: true },
},
},
})
console.warn(`🛒 SELLER ORDERS TO FULFILLMENT: ${sellerDeliveredOrders.length}`)
const productsCount = sellerDeliveredOrders.reduce(
(sum, order) =>
sum +
order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'PRODUCT' ? item.quantity : 0), 0),
0,
)
// Изменения товаров за сутки (от селлеров)
const recentSellerDeliveredOrders = await prisma.supplyOrder.findMany({
where: {
fulfillmentCenterId: organizationId, // К нам
organizationId: { not: organizationId }, // От селлеров
status: 'DELIVERED',
updatedAt: { gte: oneDayAgo },
},
include: {
items: {
include: { product: true },
},
},
})
const productsChangeToday = recentSellerDeliveredOrders.reduce(
(sum, order) =>
sum +
order.items.reduce((itemSum, item) => itemSum + (item.product.type === 'PRODUCT' ? item.quantity : 0), 0),
0,
)
// Товары (готовые товары = все продукты, не расходники)
const goodsCount = productsCount // Готовые товары = все продукты
const goodsChangeToday = productsChangeToday // Изменения товаров = изменения продуктов
// Брак
const defectsCount = 0 // TODO: реальные данные о браке
const defectsChangeToday = 0
// Возвраты с ПВЗ
const pvzReturnsCount = 0 // TODO: реальные данные о возвратах
const pvzReturnsChangeToday = 0
// Расходники фулфилмента - заказы ОТ фулфилмента К поставщикам, НО доставленные на склад фулфилмента
// Согласно правилам: фулфилмент заказывает расходники у поставщиков для своих операционных нужд
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
where: {
organizationId: organizationId, // Заказчик = фулфилмент
fulfillmentCenterId: organizationId, // ИСПРАВЛЕНО: доставлено НА склад фулфилмента
status: 'DELIVERED',
},
include: {
items: {
include: { product: true },
},
},
})
console.warn(`🏭 FULFILLMENT SUPPLY ORDERS: ${fulfillmentSupplyOrders.length}`)
// V2 СИСТЕМА: Подсчитываем количество из fulfillmentConsumableInventory
const fulfillmentSuppliesFromWarehouse = await prisma.fulfillmentConsumableInventory.findMany({
where: {
fulfillmentCenterId: organizationId, // Склад фулфилмента
},
include: {
product: true,
},
})
const fulfillmentSuppliesCount = fulfillmentSuppliesFromWarehouse.reduce(
(sum, item) => sum + (item.currentStock || 0),
0,
)
console.warn(
`🔥 FULFILLMENT SUPPLIES DEBUG: organizationId=${organizationId}, ordersCount=${fulfillmentSupplyOrders.length}, warehouseCount=${fulfillmentSuppliesFromWarehouse.length}, totalStock=${fulfillmentSuppliesCount}`,
)
console.warn(
'📦 FULFILLMENT SUPPLIES BREAKDOWN (V2):',
fulfillmentSuppliesFromWarehouse.map((item) => ({
name: item.product.name,
currentStock: item.currentStock,
totalReceived: item.totalReceived,
productId: item.product.id,
})),
)
// V2 СИСТЕМА: Изменения расходников фулфилмента за сутки (ПРИБЫЛО)
// Ищем поставки расходников V2, обновлённые за последние сутки
const fulfillmentSuppliesReceivedTodayV2 = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
fulfillmentCenterId: organizationId,
status: 'DELIVERED',
updatedAt: { gte: oneDayAgo },
},
include: {
items: {
include: { product: true },
},
},
})
const fulfillmentSuppliesChangeToday = fulfillmentSuppliesReceivedTodayV2.reduce(
(sum, supplyOrder) =>
sum +
supplyOrder.items.reduce((itemSum, item) => itemSum + (item.receivedQuantity || 0), 0),
0,
)
console.warn(
`📊 FULFILLMENT SUPPLIES RECEIVED TODAY V2 (ПРИБЫЛО): ${fulfillmentSuppliesReceivedTodayV2.length} orders, ${fulfillmentSuppliesChangeToday} items`,
)
// V2: Расходники селлеров - получаем из SellerConsumableInventory
const sellerInventoryFromWarehouse = await prisma.sellerConsumableInventory.findMany({
where: {
fulfillmentCenterId: organizationId, // Склад фулфилмента
},
})
const sellerSuppliesCount = sellerInventoryFromWarehouse.reduce(
(sum, item) => sum + (item.currentStock || 0),
0,
)
console.warn(`💼 SELLER SUPPLIES V2 DEBUG: totalCount=${sellerSuppliesCount} (from SellerConsumableInventory)`)
// V2: Изменения расходников селлеров за сутки - считаем поступления за сутки
const sellerSuppliesReceivedTodayV2 = await prisma.sellerConsumableInventory.findMany({
where: {
fulfillmentCenterId: organizationId,
lastSupplyDate: { gte: oneDayAgo }, // Пополнены за последние сутки
},
})
const sellerSuppliesChangeToday = sellerSuppliesReceivedTodayV2.reduce(
(sum, item) => sum + (item.totalReceived || 0),
0,
)
console.warn(
`📊 SELLER SUPPLIES RECEIVED TODAY V2: ${sellerSuppliesReceivedTodayV2.length} supplies, ${sellerSuppliesChangeToday} items`,
)
// Вычисляем процентные изменения
const calculatePercentChange = (current: number, change: number): number => {
if (current === 0) return change > 0 ? 100 : 0
return (change / current) * 100
}
const result = {
products: {
current: productsCount,
change: productsChangeToday,
percentChange: calculatePercentChange(productsCount, productsChangeToday),
},
goods: {
current: goodsCount,
change: goodsChangeToday,
percentChange: calculatePercentChange(goodsCount, goodsChangeToday),
},
defects: {
current: defectsCount,
change: defectsChangeToday,
percentChange: calculatePercentChange(defectsCount, defectsChangeToday),
},
pvzReturns: {
current: pvzReturnsCount,
change: pvzReturnsChangeToday,
percentChange: calculatePercentChange(pvzReturnsCount, pvzReturnsChangeToday),
},
fulfillmentSupplies: {
current: fulfillmentSuppliesCount,
change: fulfillmentSuppliesChangeToday,
percentChange: calculatePercentChange(fulfillmentSuppliesCount, fulfillmentSuppliesChangeToday),
},
sellerSupplies: {
current: sellerSuppliesCount,
change: sellerSuppliesChangeToday,
percentChange: calculatePercentChange(sellerSuppliesCount, sellerSuppliesChangeToday),
},
}
console.warn('🏁 FINAL WAREHOUSE STATS RESULT:', JSON.stringify(result, null, 2))
return result
},
// Движения товаров (прибыло/убыло) за период
supplyMovements: async (_: unknown, args: { period?: string }, context: Context) => {
console.warn('🔄 SUPPLY MOVEMENTS RESOLVER CALLED with period:', args.period)
if (!context.user) {
throw new 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
console.warn(`🏢 SUPPLY MOVEMENTS for organization: ${organizationId}`)
// Определяем период (по умолчанию 24 часа)
const periodHours = args.period === '7d' ? 168 : args.period === '30d' ? 720 : 24
const periodAgo = new Date(Date.now() - periodHours * 60 * 60 * 1000)
// ПРИБЫЛО: Поставки НА фулфилмент (статус DELIVERED за период)
const arrivedOrders = await prisma.supplyOrder.findMany({
where: {
fulfillmentCenterId: organizationId,
status: 'DELIVERED',
updatedAt: { gte: periodAgo },
},
include: {
items: {
include: { product: true },
},
},
})
console.warn(`📦 ARRIVED ORDERS: ${arrivedOrders.length}`)
// Подсчитываем прибыло по типам
const arrived = {
products: 0,
goods: 0,
defects: 0,
pvzReturns: 0,
fulfillmentSupplies: 0,
sellerSupplies: 0,
}
arrivedOrders.forEach((order) => {
order.items.forEach((item) => {
const quantity = item.quantity
const productType = item.product?.type
if (productType === 'PRODUCT') arrived.products += quantity
else if (productType === 'GOODS') arrived.goods += quantity
else if (productType === 'DEFECT') arrived.defects += quantity
else if (productType === 'PVZ_RETURN') arrived.pvzReturns += quantity
else if (productType === 'CONSUMABLE') {
// Определяем тип расходника по заказчику
if (order.organizationId === organizationId) {
arrived.fulfillmentSupplies += quantity
} else {
arrived.sellerSupplies += quantity
}
}
})
})
// УБЫЛО: Поставки НА маркетплейсы (по статусам отгрузки)
// TODO: Пока возвращаем заглушки, нужно реализовать логику отгрузок
const departed = {
products: 0, // TODO: считать из отгрузок на WB/Ozon
goods: 0,
defects: 0,
pvzReturns: 0,
fulfillmentSupplies: 0,
sellerSupplies: 0,
}
console.warn('📊 SUPPLY MOVEMENTS RESULT:', { arrived, departed })
return {
arrived,
departed,
}
},
// Логистика организации
myLogistics: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
return await prisma.logistics.findMany({
where: { organizationId: currentUser.organization.id },
include: { organization: true },
orderBy: { createdAt: 'desc' },
})
},
// Логистические партнеры (организации-логисты)
logisticsPartners: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
// Получаем все организации типа LOGIST
return await prisma.organization.findMany({
where: {
type: 'LOGIST',
// Убираем фильтр по статусу пока не определим правильные значения
},
orderBy: { createdAt: 'desc' }, // Сортируем по дате создания вместо name
})
},
// Мои поставки Wildberries
myWildberriesSupplies: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
return await prisma.wildberriesSupply.findMany({
where: { organizationId: currentUser.organization.id },
include: {
organization: true,
cards: true,
},
orderBy: { createdAt: 'desc' },
})
},
// V2: Расходники селлеров на складе фулфилмента (обновлено на V2 систему)
sellerSuppliesOnWarehouse: async (_: unknown, __: unknown, context: Context) => {
console.warn('🚀 V2 SELLER SUPPLIES ON WAREHOUSE 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('Доступ разрешен только для фулфилмент-центров')
}
try {
// V2: Получаем данные из SellerConsumableInventory вместо старой Supply таблицы
const sellerInventory = await prisma.sellerConsumableInventory.findMany({
where: {
fulfillmentCenterId: currentUser.organization.id,
},
include: {
seller: true,
fulfillmentCenter: true,
product: {
include: {
organization: true, // Поставщик товара
},
},
},
orderBy: [
{ seller: { name: 'asc' } }, // Группируем по селлерам
{ updatedAt: 'desc' },
],
})
console.warn('📊 V2 Seller Inventory loaded for warehouse:', {
fulfillmentId: currentUser.organization.id,
fulfillmentName: currentUser.organization.name,
inventoryCount: sellerInventory.length,
uniqueSellers: new Set(sellerInventory.map(item => item.sellerId)).size,
})
// Преобразуем V2 данные в формат Supply для совместимости с фронтендом
const suppliesFormatted = sellerInventory.map((item) => {
const status = item.currentStock > 0 ? 'На складе' : 'Недоступен'
const supplier = item.product.organization?.name || 'Неизвестен'
// Дополнительная проверка на null значения
if (!item.seller?.inn) {
console.error('❌ КРИТИЧЕСКАЯ ОШИБКА: seller.inn is null/undefined', {
sellerId: item.sellerId,
sellerName: item.seller?.name,
itemId: item.id,
})
}
return {
// === ИДЕНТИФИКАЦИЯ ===
id: item.id,
productId: item.product.id,
// === ОСНОВНЫЕ ДАННЫЕ ===
name: item.product.name,
article: item.product.article,
description: item.product.description || '',
unit: item.product.unit || 'шт',
category: item.product.category || 'Расходники',
imageUrl: item.product.imageUrl,
// === СКЛАДСКИЕ ДАННЫЕ ===
currentStock: item.currentStock,
minStock: item.minStock,
usedStock: item.totalUsed || 0,
quantity: item.totalReceived,
reservedStock: item.reservedStock,
// === ЦЕНЫ ===
price: parseFloat(item.averageCost.toString()),
pricePerUnit: item.usagePrice ? parseFloat(item.usagePrice.toString()) : null,
// === СТАТУС И МЕТАДАННЫЕ ===
status,
isAvailable: item.currentStock > 0,
supplier,
type: 'SELLER_CONSUMABLES', // Для совместимости с фронтендом
date: item.lastSupplyDate?.toISOString() || item.createdAt.toISOString(),
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
// === СВЯЗИ ===
organization: {
id: item.fulfillmentCenter.id,
name: item.fulfillmentCenter.name,
fullName: item.fulfillmentCenter.fullName,
type: item.fulfillmentCenter.type,
},
sellerOwner: {
id: item.seller.id,
name: item.seller.name || 'Неизвестно',
fullName: item.seller.fullName || item.seller.name || 'Неизвестно',
inn: item.seller.inn || 'НЕ_УКАЗАН',
type: item.seller.type,
},
sellerOwnerId: item.sellerId, // Для совместимости
// === ДОПОЛНИТЕЛЬНЫЕ ПОЛЯ ===
notes: item.notes,
actualQuantity: item.currentStock,
}
})
console.warn('✅ V2 Seller Supplies formatted for frontend:', {
count: suppliesFormatted.length,
totalStock: suppliesFormatted.reduce((sum, item) => sum + item.currentStock, 0),
lowStockItems: suppliesFormatted.filter(item => item.currentStock <= item.minStock).length,
})
return suppliesFormatted
} catch (error) {
console.error('❌ Error in V2 seller supplies on warehouse resolver:', error)
return []
}
},
// Мои товары и расходники (для поставщиков)
myProducts: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что это поставщик
if (currentUser.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Товары доступны только для поставщиков')
}
const products = await prisma.product.findMany({
where: {
organizationId: currentUser.organization.id,
// Показываем и товары, и расходники поставщика
},
include: {
category: true,
organization: true,
},
orderBy: { createdAt: 'desc' },
})
console.warn('🔥 MY_PRODUCTS RESOLVER DEBUG:', {
userId: currentUser.id,
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
organizationName: currentUser.organization.name,
totalProducts: products.length,
productTypes: products.map((p) => ({
id: p.id,
name: p.name,
article: p.article,
type: p.type,
isActive: p.isActive,
createdAt: p.createdAt,
})),
})
return products
},
// Товары на складе фулфилмента (из доставленных заказов поставок)
warehouseProducts: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: true,
},
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Товары склада доступны только для фулфилмент центров')
}
// Получаем все доставленные заказы поставок, где этот фулфилмент центр является получателем
const deliveredSupplyOrders = await prisma.supplyOrder.findMany({
where: {
fulfillmentCenterId: currentUser.organization.id,
status: 'DELIVERED', // Только доставленные заказы
},
include: {
items: {
include: {
product: {
include: {
category: true,
organization: true, // Включаем информацию о поставщике
},
},
},
},
organization: true, // Селлер, который сделал заказ
partner: true, // Поставщик товаров
},
})
// Собираем все товары из доставленных заказов
const allProducts: unknown[] = []
console.warn('🔍 Резолвер warehouseProducts (доставленные заказы):', {
currentUserId: currentUser.id,
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
deliveredOrdersCount: deliveredSupplyOrders.length,
orders: deliveredSupplyOrders.map((order) => ({
id: order.id,
sellerName: order.organization.name || order.organization.fullName,
supplierName: order.partner.name || order.partner.fullName,
status: order.status,
itemsCount: order.items.length,
deliveryDate: order.deliveryDate,
})),
})
for (const order of deliveredSupplyOrders) {
console.warn(
`📦 Заказ от селлера ${order.organization.name} у поставщика ${order.partner.name}:`,
order.items.map((item) => ({
productId: item.product.id,
productName: item.product.name,
article: item.product.article,
orderedQuantity: item.quantity,
price: item.price,
})),
)
for (const item of order.items) {
// Добавляем только товары типа PRODUCT, исключаем расходники
if (item.product.type === 'PRODUCT') {
allProducts.push({
...item.product,
// Дополнительная информация о заказе
orderedQuantity: item.quantity,
orderedPrice: item.price,
orderId: order.id,
orderDate: order.deliveryDate,
seller: order.organization, // Селлер, который заказал
supplier: order.partner, // Поставщик товара
// Для совместимости с существующим интерфейсом
organization: order.organization, // Указываем селлера как владельца
})
} else {
console.warn('🚫 Исключен расходник из основного склада фулфилмента:', {
name: item.product.name,
type: item.product.type,
orderId: order.id,
})
}
}
}
console.warn('✅ Итого товаров на складе фулфилмента (из доставленных заказов):', allProducts.length)
return allProducts
},
// Данные склада с партнерами (3-уровневая иерархия)
warehouseData: 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('Данные склада доступны только для фулфилмент центров')
}
console.warn('🏪 WAREHOUSE DATA: Получение данных склада для фулфилмента', currentUser.organization.id)
// Получаем всех партнеров-селлеров
const counterparties = await prisma.counterparty.findMany({
where: {
organizationId: currentUser.organization.id,
},
include: {
counterparty: true,
},
})
const sellerPartners = counterparties.filter((c) => c.counterparty.type === 'SELLER')
console.warn('🤝 PARTNERS FOUND:', {
totalCounterparties: counterparties.length,
sellerPartners: sellerPartners.length,
sellers: sellerPartners.map((p) => ({
id: p.counterparty.id,
name: p.counterparty.name,
fullName: p.counterparty.fullName,
inn: p.counterparty.inn,
})),
})
// Создаем данные склада для каждого партнера-селлера
const stores = sellerPartners.map((partner) => {
const org = partner.counterparty
// ЛОГИКА ОПРЕДЕЛЕНИЯ НАЗВАНИЯ МАГАЗИНА:
// 1. Если есть name и оно не содержит "ИП" - используем name
// 2. Если есть fullName и name содержит "ИП" - извлекаем из fullName название в скобках
// 3. Fallback к name или fullName
let storeName = org.name
if (org.fullName && org.name?.includes('ИП')) {
// Извлекаем название из скобок, например: "ИП Антипова Д. В. (Renrel)" -> "Renrel"
const match = org.fullName.match(/\(([^)]+)\)/)
if (match && match[1]) {
storeName = match[1]
}
}
return {
id: `store_${org.id}`,
storeName: storeName || org.fullName || org.name,
storeOwner: org.inn || org.fullName || org.name,
storeImage: org.logoUrl || null,
storeQuantity: 0, // Пока без поставок
partnershipDate: partner.createdAt || new Date(),
products: [], // Пустой массив продуктов
}
})
// Сортировка: новые партнеры (quantity = 0) в самом верху
stores.sort((a, b) => {
if (a.storeQuantity === 0 && b.storeQuantity > 0) return -1
if (a.storeQuantity > 0 && b.storeQuantity === 0) return 1
return b.storeQuantity - a.storeQuantity
})
console.warn('📦 WAREHOUSE STORES CREATED:', {
storesCount: stores.length,
storesPreview: stores.slice(0, 3).map((s) => ({
storeName: s.storeName,
storeOwner: s.storeOwner,
storeQuantity: s.storeQuantity,
})),
})
return {
stores,
}
},
// Все товары и расходники поставщиков для маркета
allProducts: async (_: unknown, args: { search?: string; category?: string }, context: Context) => {
console.warn('🛍️ ALL_PRODUCTS RESOLVER - ВЫЗВАН:', {
userId: context.user?.id,
search: args.search,
category: args.category,
timestamp: new Date().toISOString(),
})
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const where: Record<string, unknown> = {
isActive: true, // Показываем только активные товары
// Показываем и товары, и расходники поставщиков
organization: {
type: 'WHOLESALE', // Только товары поставщиков
},
}
if (args.search) {
where.OR = [
{ name: { contains: args.search, mode: 'insensitive' } },
{ article: { contains: args.search, mode: 'insensitive' } },
{ description: { contains: args.search, mode: 'insensitive' } },
{ brand: { contains: args.search, mode: 'insensitive' } },
]
}
if (args.category) {
where.categoryId = args.category
}
const products = await prisma.product.findMany({
where,
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
orderBy: { createdAt: 'desc' },
take: 100, // Ограничиваем количество результатов
})
console.warn('🔥 ALL_PRODUCTS RESOLVER DEBUG:', {
searchArgs: args,
whereCondition: where,
totalProducts: products.length,
productTypes: products.map((p) => ({
id: p.id,
name: p.name,
type: p.type,
org: p.organization.name,
})),
})
return products
},
// Товары конкретной организации (для формы создания поставки)
organizationProducts: async (
_: unknown,
args: { organizationId: string; search?: string; category?: string; type?: string },
context: Context,
) => {
console.warn('🏢 ORGANIZATION_PRODUCTS RESOLVER - ВЫЗВАН:', {
userId: context.user?.id,
organizationId: args.organizationId,
search: args.search,
category: args.category,
type: args.type,
timestamp: new Date().toISOString(),
})
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const where: Record<string, unknown> = {
isActive: true, // Показываем только активные товары
organizationId: args.organizationId, // Фильтруем по конкретной организации
type: args.type || 'ТОВАР', // Показываем только товары по умолчанию, НЕ расходники согласно development-checklist.md
}
if (args.search) {
where.OR = [
{ name: { contains: args.search, mode: 'insensitive' } },
{ article: { contains: args.search, mode: 'insensitive' } },
{ description: { contains: args.search, mode: 'insensitive' } },
{ brand: { contains: args.search, mode: 'insensitive' } },
]
}
if (args.category) {
where.categoryId = args.category
}
const products = await prisma.product.findMany({
where,
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
orderBy: { createdAt: 'desc' },
take: 100, // Ограничиваем количество результатов
})
console.warn('🔥 ORGANIZATION_PRODUCTS RESOLVER DEBUG:', {
organizationId: args.organizationId,
searchArgs: args,
whereCondition: where,
totalProducts: products.length,
productTypes: products.map((p) => ({
id: p.id,
name: p.name,
type: p.type,
isActive: p.isActive,
})),
})
return products
},
// Все категории
categories: async (_: unknown, __: unknown, context: Context) => {
if (!context.user && !context.admin) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
return await prisma.category.findMany({
orderBy: { name: 'asc' },
})
},
// Публичные услуги контрагента (для фулфилмента)
counterpartyServices: async (_: unknown, args: { organizationId: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что запрашиваемая организация является контрагентом
const counterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.organizationId,
},
})
if (!counterparty) {
throw new GraphQLError('Организация не является вашим контрагентом')
}
// Проверяем, что это фулфилмент центр
const targetOrganization = await prisma.organization.findUnique({
where: { id: args.organizationId },
})
if (!targetOrganization || targetOrganization.type !== 'FULFILLMENT') {
throw new GraphQLError('Услуги доступны только у фулфилмент центров')
}
return await prisma.service.findMany({
where: { organizationId: args.organizationId },
include: { organization: true },
orderBy: { createdAt: 'desc' },
})
},
// Публичные расходники контрагента (для поставщиков)
counterpartySupplies: async (_: unknown, args: { organizationId: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что запрашиваемая организация является контрагентом
const counterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.organizationId,
},
})
if (!counterparty) {
throw new GraphQLError('Организация не является вашим контрагентом')
}
// Проверяем, что это фулфилмент центр (у них есть расходники)
const targetOrganization = await prisma.organization.findUnique({
where: { id: args.organizationId },
})
if (!targetOrganization || targetOrganization.type !== 'FULFILLMENT') {
throw new GraphQLError('Расходники доступны только у фулфилмент центров')
}
// V2 СИСТЕМА А: Получаем расходники из каталога услуг фулфилмента
const consumables = await prisma.fulfillmentConsumable.findMany({
where: {
fulfillmentId: args.organizationId,
isAvailable: true,
pricePerUnit: { gt: 0 }, // Только с установленными ценами для селлеров
},
include: {
fulfillment: true,
},
orderBy: { sortOrder: 'asc' },
})
console.warn('🔥 COUNTERPARTY SUPPLIES - V2 СИСТЕМА А:', {
organizationId: args.organizationId,
consumablesCount: consumables.length,
consumablesWithPrices: consumables.filter(c => c.pricePerUnit > 0).length,
})
// Преобразуем V2 Система А в формат V1 для обратной совместимости
return consumables.map((consumable) => ({
id: consumable.id,
name: consumable.nameForSeller || consumable.name, // Используем nameForSeller если установлено
description: consumable.description || '',
price: parseFloat(consumable.pricePerUnit.toString()), // Цена для селлеров из Системы А
quantity: consumable.currentStock, // Текущий остаток
unit: consumable.unit, // Единица измерения
category: 'CONSUMABLE',
status: 'AVAILABLE',
imageUrl: consumable.imageUrl,
createdAt: consumable.createdAt,
updatedAt: consumable.updatedAt,
organization: consumable.fulfillment,
}))
},
// Корзина пользователя
myCart: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Найти или создать корзину для организации
let cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id },
include: {
items: {
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
},
},
},
organization: true,
},
})
if (!cart) {
cart = await prisma.cart.create({
data: {
organizationId: currentUser.organization.id,
},
include: {
items: {
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
},
},
},
organization: true,
},
})
}
return cart
},
// Избранные товары пользователя
myFavorites: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Получаем избранные товары
const favorites = await prisma.favorites.findMany({
where: { organizationId: currentUser.organization.id },
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
},
},
orderBy: { createdAt: 'desc' },
})
return favorites.map((favorite) => favorite.product)
},
// Сотрудники организации
myEmployees: async (_: unknown, __: unknown, context: Context) => {
console.warn('🔍 myEmployees resolver called')
if (!context.user) {
console.warn('❌ No user in context for myEmployees')
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
console.warn('✅ User authenticated for myEmployees:', context.user.id)
try {
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
console.warn('❌ User has no organization')
throw new GraphQLError('У пользователя нет организации')
}
console.warn('📊 User organization type:', currentUser.organization.type)
if (currentUser.organization.type !== 'FULFILLMENT') {
console.warn('❌ Not a fulfillment center')
throw new GraphQLError('Доступно только для фулфилмент центров')
}
const employees = await prisma.employee.findMany({
where: { organizationId: currentUser.organization.id },
include: {
organization: true,
},
orderBy: { createdAt: 'desc' },
})
console.warn('👥 Found employees:', employees.length)
return employees
} catch (error) {
console.error('❌ Error in myEmployees resolver:', error)
throw error
}
},
// Получение сотрудника по ID
employee: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
}
const employee = await prisma.employee.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
include: {
organization: true,
},
})
return employee
},
// Получить табель сотрудника за месяц
employeeSchedule: async (
_: unknown,
args: { employeeId: string; year: number; month: number },
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
}
// Проверяем что сотрудник принадлежит организации
const employee = await prisma.employee.findFirst({
where: {
id: args.employeeId,
organizationId: currentUser.organization.id,
},
})
if (!employee) {
throw new GraphQLError('Сотрудник не найден')
}
// Получаем записи табеля за указанный месяц
const startDate = new Date(args.year, args.month, 1)
const endDate = new Date(args.year, args.month + 1, 0)
const scheduleRecords = await prisma.employeeSchedule.findMany({
where: {
employeeId: args.employeeId,
date: {
gte: startDate,
lte: endDate,
},
},
orderBy: {
date: 'asc',
},
})
return scheduleRecords
},
// Получить партнерскую ссылку текущего пользователя
myPartnerLink: async (_: unknown, __: unknown, context: Context) => {
if (!context.user?.organizationId) {
throw new GraphQLError('Требуется авторизация и организация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const organization = await prisma.organization.findUnique({
where: { id: context.user.organizationId },
select: { referralCode: true },
})
if (!organization?.referralCode) {
throw new GraphQLError('Реферальный код не найден')
}
return `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/register?partner=${organization.referralCode}`
},
// Получить реферальную ссылку
myReferralLink: async (_: unknown, __: unknown, context: Context) => {
if (!context.user?.organizationId) {
return 'http://localhost:3000/register?ref=PLEASE_LOGIN'
}
const organization = await prisma.organization.findUnique({
where: { id: context.user.organizationId },
select: { referralCode: true },
})
if (!organization?.referralCode) {
throw new GraphQLError('Реферальный код не найден')
}
return `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/register?ref=${organization.referralCode}`
},
// Статистика по рефералам
myReferralStats: async (_: unknown, __: unknown, context: Context) => {
if (!context.user?.organizationId) {
throw new GraphQLError('Требуется авторизация и организация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
// Получаем текущие реферальные очки организации
const organization = await prisma.organization.findUnique({
where: { id: context.user.organizationId },
select: { referralPoints: true },
})
// Получаем все транзакции где эта организация - реферер
const transactions = await prisma.referralTransaction.findMany({
where: { referrerId: context.user.organizationId },
include: {
referral: {
select: {
type: true,
createdAt: true,
},
},
},
})
// Подсчитываем статистику
const totalSpheres = organization?.referralPoints || 0
const totalPartners = transactions.length
// Партнеры за последний месяц
const lastMonth = new Date()
lastMonth.setMonth(lastMonth.getMonth() - 1)
const monthlyPartners = transactions.filter((tx) => tx.createdAt > lastMonth).length
const monthlySpheres = transactions
.filter((tx) => tx.createdAt > lastMonth)
.reduce((sum, tx) => sum + tx.points, 0)
// Группировка по типам организаций
const typeStats: Record<string, { count: number; spheres: number }> = {}
transactions.forEach((tx) => {
const type = tx.referral.type
if (!typeStats[type]) {
typeStats[type] = { count: 0, spheres: 0 }
}
typeStats[type].count++
typeStats[type].spheres += tx.points
})
// Группировка по источникам
const sourceStats: Record<string, { count: number; spheres: number }> = {}
transactions.forEach((tx) => {
const source = tx.type === 'REGISTRATION' ? 'REFERRAL_LINK' : 'AUTO_BUSINESS'
if (!sourceStats[source]) {
sourceStats[source] = { count: 0, spheres: 0 }
}
sourceStats[source].count++
sourceStats[source].spheres += tx.points
})
return {
totalPartners,
totalSpheres,
monthlyPartners,
monthlySpheres,
referralsByType: [
{ type: 'SELLER', count: typeStats['SELLER']?.count || 0, spheres: typeStats['SELLER']?.spheres || 0 },
{
type: 'WHOLESALE',
count: typeStats['WHOLESALE']?.count || 0,
spheres: typeStats['WHOLESALE']?.spheres || 0,
},
{
type: 'FULFILLMENT',
count: typeStats['FULFILLMENT']?.count || 0,
spheres: typeStats['FULFILLMENT']?.spheres || 0,
},
{ type: 'LOGIST', count: typeStats['LOGIST']?.count || 0, spheres: typeStats['LOGIST']?.spheres || 0 },
],
referralsBySource: [
{
source: 'REFERRAL_LINK',
count: sourceStats['REFERRAL_LINK']?.count || 0,
spheres: sourceStats['REFERRAL_LINK']?.spheres || 0,
},
{
source: 'AUTO_BUSINESS',
count: sourceStats['AUTO_BUSINESS']?.count || 0,
spheres: sourceStats['AUTO_BUSINESS']?.spheres || 0,
},
],
}
} catch (error) {
console.error('Ошибка получения статистики рефералов:', error)
// Возвращаем заглушку в случае ошибки
return {
totalPartners: 0,
totalSpheres: 0,
monthlyPartners: 0,
monthlySpheres: 0,
referralsByType: [
{ type: 'SELLER', count: 0, spheres: 0 },
{ type: 'WHOLESALE', count: 0, spheres: 0 },
{ type: 'FULFILLMENT', count: 0, spheres: 0 },
{ type: 'LOGIST', count: 0, spheres: 0 },
],
referralsBySource: [
{ source: 'REFERRAL_LINK', count: 0, spheres: 0 },
{ source: 'AUTO_BUSINESS', count: 0, spheres: 0 },
],
}
}
},
// Получить список рефералов
myReferrals: async (_: unknown, args: any, context: Context) => {
if (!context.user?.organizationId) {
throw new GraphQLError('Требуется авторизация и организация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const { limit = 50, offset = 0 } = args || {}
// Получаем рефералов (организации, которых пригласил текущий пользователь)
const referralTransactions = await prisma.referralTransaction.findMany({
where: { referrerId: context.user.organizationId },
include: {
referral: {
select: {
id: true,
name: true,
fullName: true,
inn: true,
type: true,
createdAt: true,
},
},
},
orderBy: { createdAt: 'desc' },
skip: offset,
take: limit,
})
// Преобразуем в формат для UI
const referrals = referralTransactions.map((tx) => ({
id: tx.id,
organization: tx.referral,
source: tx.type === 'REGISTRATION' ? 'REFERRAL_LINK' : 'AUTO_BUSINESS',
spheresEarned: tx.points,
registeredAt: tx.createdAt.toISOString(),
status: 'ACTIVE',
}))
// Получаем общее количество для пагинации
const totalCount = await prisma.referralTransaction.count({
where: { referrerId: context.user.organizationId },
})
const totalPages = Math.ceil(totalCount / limit)
return {
referrals,
totalCount,
totalPages,
}
} catch (error) {
console.error('Ошибка получения рефералов:', error)
return {
referrals: [],
totalCount: 0,
totalPages: 0,
}
}
},
// Получить историю транзакций рефералов
myReferralTransactions: async (_: unknown, args: { limit?: number; offset?: number }, context: Context) => {
if (!context.user?.organizationId) {
throw new GraphQLError('Требуется авторизация и организация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
// Временная заглушка для отладки
const result = {
transactions: [],
totalCount: 0,
}
return result
} catch (error) {
console.error('Ошибка получения транзакций рефералов:', error)
return {
transactions: [],
totalCount: 0,
}
}
},
// 🔒 Мои поставки с системой безопасности (многоуровневая таблица)
mySupplyOrders: 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 securityContext = createSecurityContext({
user: {
id: currentUser.id,
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
},
req: context.req,
})
console.warn('🔍 GET MY SUPPLY ORDERS (SECURE):', {
userId: context.user.id,
organizationType: currentUser.organization.type,
organizationId: currentUser.organization.id,
securityEnabled: true,
})
try {
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
await ParticipantIsolation.validateAccess(
prisma,
currentUser.organization.id,
currentUser.organization.type,
'SUPPLY_ORDER',
)
// Определяем логику фильтрации в зависимости от типа организации
let whereClause
if (currentUser.organization.type === 'WHOLESALE') {
// Поставщик видит заказы, где он является поставщиком (partnerId)
whereClause = {
partnerId: currentUser.organization.id,
}
} else {
// Остальные (SELLER, FULFILLMENT) видят заказы, которые они создали (organizationId)
whereClause = {
organizationId: currentUser.organization.id,
}
}
const supplyOrders = await prisma.supplyOrder.findMany({
where: whereClause,
include: {
partner: true, // Поставщик (уровень 3)
organization: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
// Товары (уровень 4)
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
orderBy: {
createdAt: 'asc',
},
},
},
orderBy: {
createdAt: 'desc', // Новые поставки сверху (по номеру)
},
})
console.warn('📦 Найдено поставок (до фильтрации):', supplyOrders.length, {
organizationType: currentUser.organization.type,
filterType: currentUser.organization.type === 'WHOLESALE' ? 'partnerId' : 'organizationId',
organizationId: currentUser.organization.id,
})
// 🔒 ПРИМЕНЕНИЕ СИСТЕМЫ БЕЗОПАСНОСТИ К КАЖДОМУ ЗАКАЗУ
const secureProcessedOrders = await Promise.all(
supplyOrders.map(async (order) => {
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
await CommercialDataAudit.logAccess(prisma, {
userId: currentUser.id,
organizationType: currentUser.organization.type,
action: 'VIEW_PRICE',
resourceType: 'SUPPLY_ORDER',
resourceId: order.id,
metadata: {
orderStatus: order.status,
totalAmount: order.totalAmount,
partner: order.partner?.name || order.partner?.inn,
},
ipAddress: securityContext.ipAddress,
userAgent: securityContext.userAgent,
})
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ПО РОЛИ
const filteredOrder = SupplyDataFilter.filterSupplyOrder(order, securityContext)
// Обрабатываем каждый товар для получения рецептуры с фильтрацией
const processedItems = await Promise.all(
filteredOrder.data.items.map(async (item: any) => {
let recipe = null
// Получаем развернутую рецептуру если есть данные
if (
item.services?.length > 0 ||
item.fulfillmentConsumables?.length > 0 ||
item.sellerConsumables?.length > 0
) {
// 🔒 АУДИТ ДОСТУПА К РЕЦЕПТУРЕ
await CommercialDataAudit.logAccess(prisma, {
userId: currentUser.id,
organizationType: currentUser.organization.type,
action: 'VIEW_RECIPE',
resourceType: 'SUPPLY_ORDER',
resourceId: item.id,
metadata: {
hasServices: item.services?.length > 0,
hasFulfillmentConsumables: item.fulfillmentConsumables?.length > 0,
hasSellerConsumables: item.sellerConsumables?.length > 0,
},
ipAddress: securityContext.ipAddress,
userAgent: securityContext.userAgent,
})
// Получаем услуги с фильтрацией
const services =
item.services?.length > 0
? await prisma.service.findMany({
where: { id: { in: item.services } },
include: { organization: true },
})
: []
// Получаем расходники фулфилмента с фильтрацией
const fulfillmentConsumables =
item.fulfillmentConsumables?.length > 0
? await prisma.supply.findMany({
where: { id: { in: item.fulfillmentConsumables } },
include: { organization: true },
})
: []
// Получаем расходники селлера с фильтрацией
const sellerConsumables =
item.sellerConsumables?.length > 0
? await prisma.supply.findMany({
where: { id: { in: item.sellerConsumables } },
})
: []
// 🔒 ФИЛЬТРАЦИЯ РЕЦЕПТУРЫ ПО РОЛИ
// Для WHOLESALE скрываем рецептуру полностью
if (currentUser.organization.type === 'WHOLESALE') {
recipe = null
} else {
recipe = {
services,
fulfillmentConsumables,
sellerConsumables,
marketplaceCardId: item.marketplaceCardId,
}
}
}
return {
...item,
price: item.price || 0, // Исправлено: защита от null значения в существующих данных
recipe,
}
}),
)
return {
...filteredOrder.data,
items: processedItems,
// 🔒 ДОБАВЛЯЕМ МЕТАДАННЫЕ БЕЗОПАСНОСТИ
_security: {
filtered: filteredOrder.filtered,
removedFields: filteredOrder.removedFields,
accessLevel: filteredOrder.accessLevel,
},
}
}),
)
console.warn('✅ Данные обработаны с системой безопасности:', {
ordersTotal: secureProcessedOrders.length,
securityApplied: true,
organizationType: currentUser.organization.type,
})
return secureProcessedOrders
} catch (error) {
console.error('❌ Ошибка получения поставок (security):', error)
throw new GraphQLError(`Ошибка получения поставок: ${error instanceof Error ? error.message : String(error)}`)
}
},
// ВОССТАНОВЛЕННАЯ система поставок v2 из бэкапов
...fulfillmentConsumableV2QueriesRestored,
// V2 система поставок для логистики
...logisticsConsumableV2Queries,
// V2 система поставок расходников селлера
...sellerConsumableQueries,
// Новая система складских остатков V2 (заменяет старый myFulfillmentSupplies)
...fulfillmentInventoryV2Queries,
// V2 система услуг фулфилмента (включая myFulfillmentConsumables)
...fulfillmentServicesQueries,
// V2 система складских остатков расходников селлера
...sellerInventoryV2Queries,
// V2 система товарных поставок селлера
...sellerGoodsQueries,
},
Mutation: {
sendSmsCode: async (_: unknown, args: { phone: string }) => {
const result = await smsService.sendSmsCode(args.phone)
return {
success: result.success,
message: result.message || 'SMS код отправлен',
}
},
verifySmsCode: async (_: unknown, args: { phone: string; code: string }) => {
const verificationResult = await smsService.verifySmsCode(args.phone, args.code)
if (!verificationResult.success) {
return {
success: false,
message: verificationResult.message || 'Неверный код',
}
}
// Найти или создать пользователя
const formattedPhone = args.phone.replace(/\D/g, '')
let user = await prisma.user.findUnique({
where: { phone: formattedPhone },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
if (!user) {
user = await prisma.user.create({
data: {
phone: formattedPhone,
},
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
}
const token = generateToken({
userId: user.id,
phone: user.phone,
})
console.warn('verifySmsCode - Generated token:', token ? `${token.substring(0, 20)}...` : 'No token')
console.warn('verifySmsCode - Full token:', token)
console.warn('verifySmsCode - User object:', {
id: user.id,
phone: user.phone,
})
const result = {
success: true,
message: 'Авторизация успешна',
token,
user,
}
console.warn('verifySmsCode - Returning result:', {
success: result.success,
hasToken: !!result.token,
hasUser: !!result.user,
message: result.message,
tokenPreview: result.token ? `${result.token.substring(0, 20)}...` : 'No token in result',
})
return result
},
verifyInn: async (_: unknown, args: { inn: string }) => {
// Валидируем ИНН
if (!dadataService.validateInn(args.inn)) {
return {
success: false,
message: 'Неверный формат ИНН',
}
}
// Получаем данные организации из DaData
const organizationData = await dadataService.getOrganizationByInn(args.inn)
if (!organizationData) {
return {
success: false,
message: 'Организация с указанным ИНН не найдена',
}
}
return {
success: true,
message: 'ИНН найден',
organization: {
name: organizationData.name,
fullName: organizationData.fullName,
address: organizationData.address,
isActive: organizationData.isActive,
},
}
},
registerFulfillmentOrganization: async (
_: unknown,
args: {
input: {
phone: string
inn: string
type: 'FULFILLMENT' | 'LOGIST' | 'WHOLESALE'
referralCode?: string
partnerCode?: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const { inn, type, referralCode, partnerCode } = 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: 'Организация с таким ИНН уже зарегистрирована',
}
}
// Генерируем уникальный реферальный код
const generatedReferralCode = await generateReferralCode()
// Создаем организацию со всеми данными из 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)),
// Реферальная система - генерируем код автоматически
referralCode: generatedReferralCode,
},
})
// Привязываем пользователя к организации
const updatedUser = await prisma.user.update({
where: { id: context.user.id },
data: { organizationId: organization.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
// Обрабатываем реферальные коды
if (referralCode) {
try {
// Находим реферера по реферальному коду
const referrer = await prisma.organization.findUnique({
where: { referralCode: referralCode },
})
if (referrer) {
// Создаем реферальную транзакцию (100 сфер)
await prisma.referralTransaction.create({
data: {
referrerId: referrer.id,
referralId: organization.id,
points: 100,
type: 'REGISTRATION',
description: `Регистрация ${type.toLowerCase()} организации по реферальной ссылке`,
},
})
// Увеличиваем счетчик сфер у реферера
await prisma.organization.update({
where: { id: referrer.id },
data: { referralPoints: { increment: 100 } },
})
// Устанавливаем связь реферала и источник регистрации
await prisma.organization.update({
where: { id: organization.id },
data: { referredById: referrer.id },
})
}
} catch {
// Error processing referral code, but continue registration
}
}
if (partnerCode) {
try {
// Находим партнера по партнерскому коду
const partner = await prisma.organization.findUnique({
where: { referralCode: partnerCode },
})
if (partner) {
// Создаем реферальную транзакцию (100 сфер)
await prisma.referralTransaction.create({
data: {
referrerId: partner.id,
referralId: organization.id,
points: 100,
type: 'AUTO_PARTNERSHIP',
description: `Регистрация ${type.toLowerCase()} организации по партнерской ссылке`,
},
})
// Увеличиваем счетчик сфер у партнера
await prisma.organization.update({
where: { id: partner.id },
data: { referralPoints: { increment: 100 } },
})
// Устанавливаем связь реферала и источник регистрации
await prisma.organization.update({
where: { id: organization.id },
data: { referredById: partner.id },
})
// Создаем партнерскую связь (автоматическое добавление в контрагенты)
await prisma.counterparty.create({
data: {
organizationId: partner.id,
counterpartyId: organization.id,
type: 'AUTO',
triggeredBy: 'PARTNER_LINK',
},
})
await prisma.counterparty.create({
data: {
organizationId: organization.id,
counterpartyId: partner.id,
type: 'AUTO',
triggeredBy: 'PARTNER_LINK',
},
})
}
} catch {
// Error processing partner code, but continue registration
}
}
return {
success: true,
message: 'Организация успешно зарегистрирована',
user: updatedUser,
}
} catch {
// Error registering fulfillment organization
return {
success: false,
message: 'Ошибка при регистрации организации',
}
}
},
registerSellerOrganization: async (
_: unknown,
args: {
input: {
phone: string
wbApiKey?: string
ozonApiKey?: string
ozonClientId?: string
referralCode?: string
partnerCode?: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const { wbApiKey, ozonApiKey, ozonClientId, referralCode, partnerCode } = 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 generatedReferralCode = await generateReferralCode()
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',
// Реферальная система - генерируем код автоматически
referralCode: generatedReferralCode,
},
})
// Добавляем 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,
},
},
},
})
// Обрабатываем реферальные коды
if (referralCode) {
try {
// Находим реферера по реферальному коду
const referrer = await prisma.organization.findUnique({
where: { referralCode: referralCode },
})
if (referrer) {
// Создаем реферальную транзакцию (100 сфер)
await prisma.referralTransaction.create({
data: {
referrerId: referrer.id,
referralId: organization.id,
points: 100,
type: 'REGISTRATION',
description: 'Регистрация селлер организации по реферальной ссылке',
},
})
// Увеличиваем счетчик сфер у реферера
await prisma.organization.update({
where: { id: referrer.id },
data: { referralPoints: { increment: 100 } },
})
// Устанавливаем связь реферала и источник регистрации
await prisma.organization.update({
where: { id: organization.id },
data: { referredById: referrer.id },
})
}
} catch {
// Error processing referral code, but continue registration
}
}
if (partnerCode) {
try {
// Находим партнера по партнерскому коду
const partner = await prisma.organization.findUnique({
where: { referralCode: partnerCode },
})
if (partner) {
// Создаем реферальную транзакцию (100 сфер)
await prisma.referralTransaction.create({
data: {
referrerId: partner.id,
referralId: organization.id,
points: 100,
type: 'AUTO_PARTNERSHIP',
description: 'Регистрация селлер организации по партнерской ссылке',
},
})
// Увеличиваем счетчик сфер у партнера
await prisma.organization.update({
where: { id: partner.id },
data: { referralPoints: { increment: 100 } },
})
// Устанавливаем связь реферала и источник регистрации
await prisma.organization.update({
where: { id: organization.id },
data: { referredById: partner.id },
})
// Создаем партнерскую связь (автоматическое добавление в контрагенты)
await prisma.counterparty.create({
data: {
organizationId: partner.id,
counterpartyId: organization.id,
type: 'AUTO',
triggeredBy: 'PARTNER_LINK',
},
})
await prisma.counterparty.create({
data: {
organizationId: organization.id,
counterpartyId: partner.id,
type: 'AUTO',
triggeredBy: 'PARTNER_LINK',
},
})
}
} catch {
// Error processing partner code, but continue registration
}
}
return {
success: true,
message: 'Селлер организация успешно зарегистрирована',
user: updatedUser,
}
} catch {
// Error registering seller organization
return {
success: false,
message: 'Ошибка при регистрации организации',
}
}
},
addMarketplaceApiKey: async (
_: unknown,
args: {
input: {
marketplace: 'WILDBERRIES' | 'OZON'
apiKey: string
clientId?: string
validateOnly?: boolean
}
},
context: Context,
) => {
// Разрешаем валидацию без авторизации
if (!args.input.validateOnly && !context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const { marketplace, apiKey, clientId, validateOnly } = args.input
console.warn(`🔍 Validating ${marketplace} API key:`, {
keyLength: apiKey.length,
keyPreview: apiKey.substring(0, 20) + '...',
validateOnly,
})
// Валидируем API ключ
const validationResult = await marketplaceService.validateApiKey(marketplace, apiKey, clientId)
console.warn(`✅ Validation result for ${marketplace}:`, validationResult)
if (!validationResult.isValid) {
console.warn(`❌ Validation failed for ${marketplace}:`, validationResult.message)
return {
success: false,
message: validationResult.message,
}
}
// Если это только валидация, возвращаем результат без сохранения
if (validateOnly) {
return {
success: true,
message: 'API ключ действителен',
apiKey: {
id: 'validate-only',
marketplace,
apiKey: '***', // Скрываем реальный ключ при валидации
isActive: true,
validationData: validationResult,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
}
}
// Для сохранения API ключа нужна авторизация
if (!context.user) {
throw new GraphQLError('Требуется авторизация для сохранения API ключа', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
return {
success: false,
message: 'Пользователь не привязан к организации',
}
}
try {
// Проверяем, что такого ключа еще нет
const existingKey = await prisma.apiKey.findUnique({
where: {
organizationId_marketplace: {
organizationId: user.organization.id,
marketplace,
},
},
})
if (existingKey) {
// Обновляем существующий ключ
const updatedKey = await prisma.apiKey.update({
where: { id: existingKey.id },
data: {
apiKey,
validationData: JSON.parse(JSON.stringify(validationResult.data)),
isActive: true,
},
})
return {
success: true,
message: 'API ключ успешно обновлен',
apiKey: updatedKey,
}
} else {
// Создаем новый ключ
const newKey = await prisma.apiKey.create({
data: {
marketplace,
apiKey,
organizationId: user.organization.id,
validationData: JSON.parse(JSON.stringify(validationResult.data)),
},
})
return {
success: true,
message: 'API ключ успешно добавлен',
apiKey: newKey,
}
}
} catch (error) {
console.error('Error adding marketplace API key:', error)
return {
success: false,
message: 'Ошибка при добавлении API ключа',
}
}
},
removeMarketplaceApiKey: async (_: unknown, args: { marketplace: 'WILDBERRIES' | 'OZON' }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Пользователь не привязан к организации')
}
try {
await prisma.apiKey.delete({
where: {
organizationId_marketplace: {
organizationId: user.organization.id,
marketplace: args.marketplace,
},
},
})
return true
} catch (error) {
console.error('Error removing marketplace API key:', error)
return false
}
},
updateUserProfile: async (
_: unknown,
args: {
input: {
avatar?: string
orgPhone?: string
managerName?: string
telegram?: string
whatsapp?: string
email?: string
bankName?: string
bik?: string
accountNumber?: string
corrAccount?: string
market?: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
if (!user?.organization) {
throw new GraphQLError('Пользователь не привязан к организации')
}
try {
const { input } = args
// Обновляем данные пользователя (аватар, имя управляющего)
const userUpdateData: { avatar?: string; managerName?: string } = {}
if (input.avatar) {
userUpdateData.avatar = input.avatar
}
if (input.managerName) {
userUpdateData.managerName = input.managerName
}
if (Object.keys(userUpdateData).length > 0) {
await prisma.user.update({
where: { id: context.user.id },
data: userUpdateData,
})
}
// Подготавливаем данные для обновления организации
const updateData: {
phones?: object
emails?: object
managementName?: string
managementPost?: string
market?: string
} = {}
// Название организации больше не обновляется через профиль
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
// Обновляем контактные данные в JSON поле phones
if (input.orgPhone) {
updateData.phones = [{ value: input.orgPhone, type: 'main' }]
}
// Обновляем email в JSON поле emails
if (input.email) {
updateData.emails = [{ value: input.email, type: 'main' }]
}
// Обновляем рынок для поставщиков
if (input.market !== undefined) {
updateData.market = input.market === 'none' ? null : input.market
}
// Сохраняем дополнительные контакты в custom полях
// Пока добавим их как дополнительные JSON поля
const customContacts: {
managerName?: string
telegram?: string
whatsapp?: string
bankDetails?: {
bankName?: string
bik?: string
accountNumber?: string
corrAccount?: string
}
} = {}
// managerName теперь сохраняется в поле пользователя, а не в JSON
if (input.telegram) {
customContacts.telegram = input.telegram
}
if (input.whatsapp) {
customContacts.whatsapp = input.whatsapp
}
if (input.bankName || input.bik || input.accountNumber || input.corrAccount) {
customContacts.bankDetails = {
bankName: input.bankName,
bik: input.bik,
accountNumber: input.accountNumber,
corrAccount: input.corrAccount,
}
}
// Если есть дополнительные контакты, сохраним их в поле managementPost временно
// В идеале нужно добавить отдельную таблицу для контактов
if (Object.keys(customContacts).length > 0) {
updateData.managementPost = JSON.stringify(customContacts)
}
// Обновляем организацию
await prisma.organization.update({
where: { id: user.organization.id },
data: updateData,
include: {
apiKeys: true,
},
})
// Получаем обновленного пользователя
const updatedUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
return {
success: true,
message: 'Профиль успешно обновлен',
user: updatedUser,
}
} catch (error) {
console.error('Error updating user profile:', error)
return {
success: false,
message: 'Ошибка при обновлении профиля',
}
}
},
updateOrganizationByInn: async (_: unknown, args: { inn: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
if (!user?.organization) {
throw new GraphQLError('Пользователь не привязан к организации')
}
try {
// Валидируем ИНН
if (!dadataService.validateInn(args.inn)) {
return {
success: false,
message: 'Неверный формат ИНН',
}
}
// Получаем данные организации из DaData
const organizationData = await dadataService.getOrganizationByInn(args.inn)
if (!organizationData) {
return {
success: false,
message: 'Организация с указанным ИНН не найдена в федеральном реестре',
}
}
// Проверяем, есть ли уже организация с таким ИНН в базе (кроме текущей)
const existingOrganization = await prisma.organization.findUnique({
where: { inn: organizationData.inn },
})
if (existingOrganization && existingOrganization.id !== user.organization.id) {
return {
success: false,
message: `Организация с ИНН ${organizationData.inn} уже существует в системе`,
}
}
// Подготавливаем данные для обновления
const updateData: Prisma.OrganizationUpdateInput = {
kpp: organizationData.kpp,
// Для селлеров не обновляем название организации (это название магазина)
...(user.organization.type !== 'SELLER' && {
name: organizationData.name,
}),
fullName: organizationData.fullName,
address: organizationData.address,
addressFull: organizationData.addressFull,
ogrn: organizationData.ogrn,
ogrnDate: organizationData.ogrnDate ? organizationData.ogrnDate.toISOString() : null,
registrationDate: organizationData.registrationDate ? organizationData.registrationDate.toISOString() : null,
liquidationDate: organizationData.liquidationDate ? organizationData.liquidationDate.toISOString() : null,
managementName: organizationData.managementName, // Всегда перезаписываем данными из DaData (может быть null)
managementPost: user.organization.managementPost, // Сохраняем кастомные данные пользователя
opfCode: organizationData.opfCode,
opfFull: organizationData.opfFull,
opfShort: organizationData.opfShort,
okato: organizationData.okato,
oktmo: organizationData.oktmo,
okpo: organizationData.okpo,
okved: organizationData.okved,
status: organizationData.status,
}
// Добавляем ИНН только если он отличается от текущего
if (user.organization.inn !== organizationData.inn) {
updateData.inn = organizationData.inn
}
// Обновляем организацию
await prisma.organization.update({
where: { id: user.organization.id },
data: updateData,
include: {
apiKeys: true,
},
})
// Получаем обновленного пользователя
const updatedUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
return {
success: true,
message: 'Данные организации успешно обновлены',
user: updatedUser,
}
} catch (error) {
console.error('Error updating organization by INN:', error)
return {
success: false,
message: 'Ошибка при обновлении данных организации',
}
}
},
logout: () => {
// В stateless JWT системе logout происходит на клиенте
// Можно добавить blacklist токенов, если нужно
return true
},
// Отправить заявку на добавление в контрагенты
sendCounterpartyRequest: async (
_: unknown,
args: { organizationId: string; message?: string },
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
if (currentUser.organization.id === args.organizationId) {
throw new GraphQLError('Нельзя отправить заявку самому себе')
}
// Проверяем, что организация-получатель существует
const receiverOrganization = await prisma.organization.findUnique({
where: { id: args.organizationId },
})
if (!receiverOrganization) {
throw new GraphQLError('Организация не найдена')
}
try {
// Создаем или обновляем заявку
const request = await prisma.counterpartyRequest.upsert({
where: {
senderId_receiverId: {
senderId: currentUser.organization.id,
receiverId: args.organizationId,
},
},
update: {
status: 'PENDING',
message: args.message,
updatedAt: new Date(),
},
create: {
senderId: currentUser.organization.id,
receiverId: args.organizationId,
message: args.message,
status: 'PENDING',
},
include: {
sender: true,
receiver: true,
},
})
// Уведомляем получателя о новой заявке
try {
notifyOrganization(args.organizationId, {
type: 'counterparty:request:new',
payload: {
requestId: request.id,
senderId: request.senderId,
receiverId: request.receiverId,
},
})
} catch {}
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,
},
}),
])
// АВТОМАТИЧЕСКОЕ СОЗДАНИЕ ЗАПИСЕЙ В ТАБЛИЦЕ СКЛАДА ФУЛФИЛМЕНТА
// Проверяем, есть ли фулфилмент среди партнеров
if (request.receiver.type === 'FULFILLMENT' && request.sender.type === 'SELLER') {
// Селлер становится партнером фулфилмента - создаем запись склада
try {
await autoCreateWarehouseEntry(request.senderId, request.receiverId)
console.warn(
`✅ AUTO WAREHOUSE ENTRY: Created for seller ${request.senderId} with fulfillment ${request.receiverId}`,
)
} catch (error) {
console.error('❌ AUTO WAREHOUSE ENTRY ERROR:', error)
// Не прерываем основной процесс, если не удалось создать запись склада
}
} else if (request.sender.type === 'FULFILLMENT' && request.receiver.type === 'SELLER') {
// Фулфилмент принимает заявку от селлера - создаем запись склада
try {
await autoCreateWarehouseEntry(request.receiverId, request.senderId)
console.warn(
`✅ AUTO WAREHOUSE ENTRY: Created for seller ${request.receiverId} with fulfillment ${request.senderId}`,
)
} catch (error) {
console.error('❌ AUTO WAREHOUSE ENTRY ERROR:', error)
}
}
}
// Оповещаем обе стороны об обновлении заявки и возможном изменении списка контрагентов
try {
notifyMany([request.senderId, request.receiverId], {
type: 'counterparty:request:updated',
payload: { requestId: updatedRequest.id, status: updatedRequest.status },
})
} catch {}
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
}
},
// Автоматическое создание записи в таблице склада
autoCreateWarehouseEntry: async (_: unknown, args: { partnerId: 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 partnerOrg = await prisma.organization.findUnique({
where: { id: args.partnerId },
})
if (!partnerOrg) {
throw new GraphQLError('Партнер не найден')
}
if (partnerOrg.type !== 'SELLER') {
throw new GraphQLError('Автозаписи создаются только для партнеров-селлеров')
}
// Создаем запись склада
const warehouseEntry = await autoCreateWarehouseEntry(args.partnerId, currentUser.organization.id)
return {
success: true,
message: 'Запись склада создана успешно',
warehouseEntry,
}
} catch (error) {
console.error('Error creating auto warehouse entry:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка создания записи склада',
}
}
},
// Отправить сообщение
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,
},
},
},
})
// Реалтайм нотификация для обеих организаций (отправитель и получатель)
try {
notifyMany([currentUser.organization.id, args.receiverOrganizationId], {
type: 'message:new',
payload: {
messageId: message.id,
senderOrgId: message.senderOrganizationId,
receiverOrgId: message.receiverOrganizationId,
type: message.type,
},
})
} catch {}
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,
},
},
},
})
try {
notifyMany([currentUser.organization.id, args.receiverOrganizationId], {
type: 'message:new',
payload: {
messageId: message.id,
senderOrgId: message.senderOrganizationId,
receiverOrgId: message.receiverOrganizationId,
type: message.type,
},
})
} catch {}
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,
},
},
},
})
try {
notifyMany([currentUser.organization.id, args.receiverOrganizationId], {
type: 'message:new',
payload: {
messageId: message.id,
senderOrgId: message.senderOrganizationId,
receiverOrgId: message.receiverOrganizationId,
type: message.type,
},
})
} catch {}
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,
},
},
},
})
try {
notifyMany([currentUser.organization.id, args.receiverOrganizationId], {
type: 'message:new',
payload: {
messageId: message.id,
senderOrgId: message.senderOrganizationId,
receiverOrgId: message.receiverOrganizationId,
type: message.type,
},
})
} catch {}
return {
success: true,
message: 'Файл отправлен',
messageData: message,
}
} catch (error) {
console.error('Error sending file:', error)
return {
success: false,
message: 'Ошибка при отправке файла',
}
}
},
// Отметить сообщения как прочитанные
markMessagesAsRead: async (_: unknown, args: { conversationId: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// conversationId имеет формат "currentOrgId-counterpartyId"
const [, counterpartyId] = args.conversationId.split('-')
if (!counterpartyId) {
throw new GraphQLError('Неверный ID беседы')
}
// Помечаем все непрочитанные сообщения от контрагента как прочитанные
await prisma.message.updateMany({
where: {
senderOrganizationId: counterpartyId,
receiverOrganizationId: currentUser.organization.id,
isRead: false,
},
data: {
isRead: true,
},
})
return true
},
// Создать услугу
createService: async (
_: unknown,
args: {
input: {
name: string
description?: string
price: number
imageUrl?: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Услуги доступны только для фулфилмент центров')
}
try {
const service = await prisma.service.create({
data: {
name: args.input.name,
description: args.input.description,
price: args.input.price,
imageUrl: args.input.imageUrl,
organizationId: currentUser.organization.id,
},
include: { organization: true },
})
return {
success: true,
message: 'Услуга успешно создана',
service,
}
} catch (error) {
console.error('Error creating service:', error)
return {
success: false,
message: 'Ошибка при создании услуги',
}
}
},
// Обновить услугу
updateService: async (
_: unknown,
args: {
id: string
input: {
name: string
description?: string
price: number
imageUrl?: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что услуга принадлежит текущей организации
const existingService = await prisma.service.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
})
if (!existingService) {
throw new GraphQLError('Услуга не найдена или нет доступа')
}
try {
const service = await prisma.service.update({
where: { id: args.id },
data: {
name: args.input.name,
description: args.input.description,
price: args.input.price,
imageUrl: args.input.imageUrl,
},
include: { organization: true },
})
return {
success: true,
message: 'Услуга успешно обновлена',
service,
}
} catch (error) {
console.error('Error updating service:', error)
return {
success: false,
message: 'Ошибка при обновлении услуги',
}
}
},
// Удалить услугу
deleteService: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что услуга принадлежит текущей организации
const existingService = await prisma.service.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
})
if (!existingService) {
throw new GraphQLError('Услуга не найдена или нет доступа')
}
try {
await prisma.service.delete({
where: { id: args.id },
})
return true
} catch (error) {
console.error('Error deleting service:', error)
return false
}
},
// Обновить цену расходника (новая архитектура - только цену можно редактировать)
updateSupplyPrice: async (
_: unknown,
args: {
id: string
input: {
pricePerUnit?: number | null
}
},
context: Context,
) => {
console.warn('🔥 UPDATE_SUPPLY_PRICE called with args:', args)
if (!context.user) {
throw new 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 {
// V2 СИСТЕМА А: Находим и обновляем расходник в каталоге услуг фулфилмента
const existingConsumable = await prisma.fulfillmentConsumable.findFirst({
where: {
id: args.id,
fulfillmentId: currentUser.organization.id,
},
include: {
fulfillment: true,
},
})
if (!existingConsumable) {
throw new GraphQLError('Расходник не найден в каталоге услуг')
}
const updatedConsumable = await prisma.fulfillmentConsumable.update({
where: { id: args.id },
data: {
pricePerUnit: args.input.pricePerUnit || 0, // Обновляем цену для селлеров в V2 Система А
updatedAt: new Date(),
},
include: {
fulfillment: true,
},
})
// Преобразуем V2 данные в формат для GraphQL
const transformedSupply = {
id: updatedConsumable.id,
name: updatedConsumable.name,
description: updatedConsumable.description || '',
pricePerUnit: parseFloat(updatedConsumable.pricePerUnit.toString()),
unit: updatedConsumable.unit,
imageUrl: updatedConsumable.imageUrl,
warehouseStock: updatedConsumable.currentStock,
isAvailable: updatedConsumable.isAvailable,
warehouseConsumableId: updatedConsumable.id,
createdAt: updatedConsumable.createdAt,
updatedAt: updatedConsumable.updatedAt,
organization: updatedConsumable.fulfillment,
}
console.warn('🔥 V2 SUPPLY PRICE UPDATED:', {
id: transformedSupply.id,
name: transformedSupply.name,
oldPrice: existingInventoryItem.resalePrice,
newPrice: transformedSupply.pricePerUnit,
})
return {
success: true,
message: 'Цена расходника успешно обновлена',
supply: transformedSupply,
}
} catch (error) {
console.error('Error updating supply price:', error)
return {
success: false,
message: 'Ошибка при обновлении цены расходника',
}
}
},
// V2 мутация для обновления цены в инвентаре фулфилмента
updateFulfillmentInventoryPrice: async (
_: unknown,
args: {
id: string
input: {
pricePerUnit?: number | null
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Обновление цен расходников доступно только для фулфилмент центров')
}
try {
const updatedConsumable = await prisma.fulfillmentConsumable.update({
where: {
id: args.id,
fulfillmentId: currentUser.organization.id,
},
data: {
pricePerUnit: args.input.pricePerUnit || 0,
updatedAt: new Date(),
},
include: {
fulfillment: true,
},
})
const transformedItem = {
id: updatedConsumable.id,
name: updatedConsumable.name,
description: updatedConsumable.description || '',
pricePerUnit: parseFloat(updatedConsumable.pricePerUnit.toString()),
unit: updatedConsumable.unit,
imageUrl: updatedConsumable.imageUrl,
warehouseStock: updatedConsumable.currentStock,
isAvailable: updatedConsumable.isAvailable,
warehouseConsumableId: updatedConsumable.id,
createdAt: updatedConsumable.createdAt,
updatedAt: updatedConsumable.updatedAt,
organization: updatedConsumable.fulfillment,
}
console.warn('🔥 V2 FULFILLMENT INVENTORY PRICE UPDATED:', {
id: transformedItem.id,
name: transformedItem.name,
newPrice: transformedItem.pricePerUnit,
})
return {
success: true,
message: 'Цена расходника успешно обновлена',
item: transformedItem,
}
} catch (error) {
console.error('Error updating fulfillment inventory price:', error)
return {
success: false,
message: 'Ошибка при обновлении цены расходника',
item: null,
}
}
},
// ❌ УДАЛЕН: useFulfillmentSupplies (V1 - использовал устаревшую таблицу Supply)
// ✅ ЗАМЕНА: используйте myFulfillmentConsumables из fulfillment-services-v2.ts
// Создать заказ поставки расходников
// Два сценария:
// 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
recipe?: {
services?: string[]
fulfillmentConsumables?: string[]
sellerConsumables?: string[]
marketplaceCardId?: string
}
}>
notes?: string // Дополнительные заметки к заказу
consumableType?: string // Классификация расходников
// Новые поля для многоуровневой системы
packagesCount?: number // Количество грузовых мест (заполняет поставщик)
volume?: number // Объём товара в м³ (заполняет поставщик)
routes?: Array<{
logisticsId?: string // Ссылка на предустановленный маршрут
fromLocation: string // Точка забора
toLocation: string // Точка доставки
fromAddress?: string // Полный адрес забора
toAddress?: string // Полный адрес доставки
}>
}
},
context: Context,
) => {
console.warn('🚀 CREATE_SUPPLY_ORDER 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 },
})
console.warn('🔍 Проверка пользователя:', {
userId: context.user.id,
userFound: !!currentUser,
organizationFound: !!currentUser?.organization,
organizationType: currentUser?.organization?.type,
organizationId: currentUser?.organization?.id,
})
if (!currentUser) {
throw new GraphQLError('Пользователь не найден')
}
if (!currentUser.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем тип организации и определяем роль в процессе поставки
const allowedTypes = ['FULFILLMENT', 'SELLER', 'LOGIST']
if (!allowedTypes.includes(currentUser.organization.type)) {
throw new GraphQLError('Заказы поставок недоступны для данного типа организации')
}
// Определяем роль организации в процессе поставки
const organizationRole = currentUser.organization.type
let fulfillmentCenterId = args.input.fulfillmentCenterId
// Если заказ создает фулфилмент-центр, он сам является получателем
if (organizationRole === 'FULFILLMENT') {
fulfillmentCenterId = currentUser.organization.id
}
// Если указан фулфилмент-центр, проверяем его существование
if (fulfillmentCenterId) {
const fulfillmentCenter = await prisma.organization.findFirst({
where: {
id: fulfillmentCenterId,
type: 'FULFILLMENT',
},
})
if (!fulfillmentCenter) {
return {
success: false,
message: 'Указанный фулфилмент-центр не найден',
}
}
}
// Проверяем, что партнер существует и является поставщиком
const partner = await prisma.organization.findFirst({
where: {
id: args.input.partnerId,
type: 'WHOLESALE',
},
})
if (!partner) {
return {
success: false,
message: 'Партнер не найден или не является поставщиком',
}
}
// Проверяем, что партнер является контрагентом
const counterparty = await prisma.counterparty.findFirst({
where: {
organizationId: currentUser.organization.id,
counterpartyId: args.input.partnerId,
},
})
if (!counterparty) {
return {
success: false,
message: 'Данная организация не является вашим партнером',
}
}
// Получаем товары для проверки наличия и цен
const productIds = args.input.items.map((item) => item.productId)
const products = await prisma.product.findMany({
where: {
id: { in: productIds },
organizationId: args.input.partnerId,
isActive: true,
},
})
if (products.length !== productIds.length) {
return {
success: false,
message: 'Некоторые товары не найдены или неактивны',
}
}
// Проверяем наличие товаров
for (const item of args.input.items) {
const product = products.find((p) => p.id === item.productId)
if (!product) {
return {
success: false,
message: `Товар ${item.productId} не найден`,
}
}
if (product.quantity < item.quantity) {
return {
success: false,
message: `Недостаточно товара "${product.name}". Доступно: ${product.quantity}, запрошено: ${item.quantity}`,
}
}
}
// Рассчитываем общую сумму и количество
let totalAmount = 0
let totalItems = 0
const orderItems = args.input.items.map((item) => {
const product = products.find((p) => p.id === item.productId)!
const itemTotal = Number(product.price) * item.quantity
totalAmount += itemTotal
totalItems += item.quantity
/* ОТКАТ: Новая логика сохранения рецептур - ЗАКОММЕНТИРОВАНО
// Получаем полные данные рецептуры из БД
let recipeData = null
if (item.recipe && (item.recipe.services?.length || item.recipe.fulfillmentConsumables?.length || item.recipe.sellerConsumables?.length)) {
// Получаем услуги фулфилмента
const services = item.recipe.services ? await context.prisma.supply.findMany({
where: { id: { in: item.recipe.services } },
select: { id: true, name: true, description: true, pricePerUnit: true }
}) : []
// Получаем расходники фулфилмента
const fulfillmentConsumables = item.recipe.fulfillmentConsumables ? await context.prisma.supply.findMany({
where: { id: { in: item.recipe.fulfillmentConsumables } },
select: { id: true, name: true, description: true, pricePerUnit: true, unit: true, imageUrl: true }
}) : []
// Получаем расходники селлера
const sellerConsumables = item.recipe.sellerConsumables ? await context.prisma.supply.findMany({
where: { id: { in: item.recipe.sellerConsumables } },
select: { id: true, name: true, description: true, pricePerUnit: true, unit: true }
}) : []
recipeData = {
services: services.map(service => ({
id: service.id,
name: service.name,
description: service.description,
price: service.pricePerUnit
})),
fulfillmentConsumables: fulfillmentConsumables.map(consumable => ({
id: consumable.id,
name: consumable.name,
description: consumable.description,
price: consumable.pricePerUnit,
unit: consumable.unit,
imageUrl: consumable.imageUrl
})),
sellerConsumables: sellerConsumables.map(consumable => ({
id: consumable.id,
name: consumable.name,
description: consumable.description,
price: consumable.pricePerUnit,
unit: consumable.unit
})),
marketplaceCardId: item.recipe.marketplaceCardId
}
}
return {
productId: item.productId,
quantity: item.quantity,
price: product.price,
totalPrice: new Prisma.Decimal(itemTotal),
// Сохраняем полную рецептуру как JSON
recipe: recipeData ? JSON.stringify(recipeData) : null,
}
*/
// ВОССТАНОВЛЕННАЯ ОРИГИНАЛЬНАЯ ЛОГИКА:
return {
productId: item.productId,
quantity: item.quantity,
price: product.price || 0, // Исправлено: защита от null значения
totalPrice: new Prisma.Decimal(itemTotal),
// Извлечение данных рецептуры из объекта recipe
services: item.recipe?.services || [],
fulfillmentConsumables: item.recipe?.fulfillmentConsumables || [],
sellerConsumables: item.recipe?.sellerConsumables || [],
marketplaceCardId: item.recipe?.marketplaceCardId,
}
})
try {
// Определяем начальный статус в зависимости от роли организации
let initialStatus: 'PENDING' | 'CONFIRMED' = 'PENDING'
if (organizationRole === 'SELLER') {
initialStatus = 'PENDING' // Селлер создает заказ, ждет подтверждения поставщика
} else if (organizationRole === 'FULFILLMENT') {
initialStatus = 'PENDING' // Фулфилмент заказывает для своего склада
} else if (organizationRole === 'LOGIST') {
initialStatus = 'CONFIRMED' // Логист может сразу подтверждать заказы
}
// ИСПРАВЛЕНИЕ: Автоматически определяем тип расходников на основе заказчика
const consumableType =
currentUser.organization.type === 'SELLER' ? 'SELLER_CONSUMABLES' : 'FULFILLMENT_CONSUMABLES'
console.warn('🔍 Автоматическое определение типа расходников:', {
organizationType: currentUser.organization.type,
consumableType: consumableType,
inputType: args.input.consumableType, // Для отладки
})
// Подготавливаем данные для создания заказа
const createData: any = {
partnerId: args.input.partnerId,
deliveryDate: new Date(args.input.deliveryDate),
totalAmount: new Prisma.Decimal(totalAmount),
totalItems: totalItems,
organizationId: currentUser.organization.id,
fulfillmentCenterId: fulfillmentCenterId,
consumableType: consumableType, // ИСПРАВЛЕНО: используем автоматически определенный тип
status: initialStatus,
// Новые поля для многоуровневой системы (пока что селлер не может задать эти поля)
// packagesCount: args.input.packagesCount || null, // Поле не существует в модели
// volume: args.input.volume || null, // Поле не существует в модели
// notes: args.input.notes || null, // Поле не существует в модели
items: {
create: orderItems,
},
}
// 🔄 ЛОГИСТИКА ОПЦИОНАЛЬНА: добавляем только если передана
if (args.input.logisticsPartnerId) {
createData.logisticsPartnerId = args.input.logisticsPartnerId
}
console.warn('🔍 Создаем SupplyOrder с данными:', {
hasLogistics: !!args.input.logisticsPartnerId,
logisticsId: args.input.logisticsPartnerId,
createData: createData,
})
const supplyOrder = await prisma.supplyOrder.create({
data: createData,
include: {
partner: {
include: {
users: true,
},
},
organization: {
include: {
users: true,
},
},
fulfillmentCenter: {
include: {
users: true,
},
},
logisticsPartner: {
include: {
users: true,
},
},
// employee: true, // Поле не существует в модели
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
},
// Маршруты будут добавлены отдельно после создания
},
})
// 📍 СОЗДАЕМ МАРШРУТЫ ПОСТАВКИ (если указаны)
if (args.input.routes && args.input.routes.length > 0) {
const routesData = args.input.routes.map((route) => ({
supplyOrderId: supplyOrder.id,
logisticsId: route.logisticsId || null,
fromLocation: route.fromLocation,
toLocation: route.toLocation,
fromAddress: route.fromAddress || null,
toAddress: route.toAddress || null,
status: 'pending',
createdDate: new Date(), // Дата создания маршрута (уровень 2)
}))
await prisma.supplyRoute.createMany({
data: routesData,
})
console.warn(`📍 Созданы маршруты для заказа ${supplyOrder.id}:`, routesData.length)
} else {
// Создаем маршрут по умолчанию на основе адресов организаций
const defaultRoute = {
supplyOrderId: supplyOrder.id,
fromLocation: partner.market || partner.address || 'Поставщик',
toLocation: fulfillmentCenterId ? 'Фулфилмент-центр' : 'Получатель',
fromAddress: partner.addressFull || partner.address || null,
toAddress: fulfillmentCenterId
? (
await prisma.organization.findUnique({
where: { id: fulfillmentCenterId },
select: { addressFull: true, address: true },
})
)?.addressFull || null
: null,
status: 'pending',
createdDate: new Date(),
}
await prisma.supplyRoute.create({
data: defaultRoute,
})
console.warn(`📍 Создан маршрут по умолчанию для заказа ${supplyOrder.id}`)
}
// Реалтайм: уведомляем поставщика и вовлеченные стороны о новом заказе
try {
const orgIds = [
currentUser.organization.id,
args.input.partnerId,
fulfillmentCenterId || undefined,
args.input.logisticsPartnerId || undefined,
].filter(Boolean) as string[]
notifyMany(orgIds, {
type: 'supply-order:new',
payload: { id: supplyOrder.id, organizationId: currentUser.organization.id },
})
} catch {}
// 📦 РЕЗЕРВИРУЕМ ТОВАРЫ У ПОСТАВЩИКА
// Увеличиваем поле "ordered" для каждого заказанного товара
for (const item of args.input.items) {
await prisma.product.update({
where: { id: item.productId },
data: {
ordered: {
increment: item.quantity,
},
},
})
}
console.warn(
`📦 Зарезервированы товары для заказа ${supplyOrder.id}:`,
args.input.items.map((item) => `${item.productId}: +${item.quantity} шт.`).join(', '),
)
// Проверяем, является ли это первой сделкой организации
const isFirstOrder =
(await prisma.supplyOrder.count({
where: {
organizationId: currentUser.organization.id,
id: { not: supplyOrder.id },
},
})) === 0
// Если это первая сделка и организация была приглашена по реферальной ссылке
if (isFirstOrder && currentUser.organization.referredById) {
try {
// Создаем транзакцию на 100 сфер за первую сделку
await prisma.referralTransaction.create({
data: {
referrerId: currentUser.organization.referredById,
referralId: currentUser.organization.id,
points: 100,
type: 'FIRST_ORDER',
description: `Первая сделка реферала ${currentUser.organization.name || currentUser.organization.inn}`,
},
})
// Увеличиваем счетчик сфер у реферера
await prisma.organization.update({
where: { id: currentUser.organization.referredById },
data: { referralPoints: { increment: 100 } },
})
console.log(`💰 Начислено 100 сфер рефереру за первую сделку организации ${currentUser.organization.id}`)
} catch (error) {
console.error('Ошибка начисления сфер за первую сделку:', error)
// Не прерываем создание заказа из-за ошибки начисления
}
}
// V2 СИСТЕМА: Расходники будут автоматически созданы при подтверждении заказа
console.warn('📦 V2 система: расходники будут созданы автоматически при доставке через соответствующие резолверы')
// 🔔 ОТПРАВЛЯЕМ УВЕДОМЛЕНИЕ ПОСТАВЩИКУ О НОВОМ ЗАКАЗЕ
try {
const orderSummary = args.input.items
.map((item) => {
const product = products.find((p) => p.id === item.productId)!
return `${product.name} - ${item.quantity} шт.`
})
.join(', ')
const notificationMessage = `🔔 Новый заказ поставки от ${
currentUser.organization.name || currentUser.organization.fullName
}!\n\nТовары: ${orderSummary}\ата доставки: ${new Date(args.input.deliveryDate).toLocaleDateString(
'ru-RU',
)}\nОбщая сумма: ${totalAmount.toLocaleString(
'ru-RU',
)}\n\ожалуйста, подтвердите заказ в разделе "Поставки".`
await prisma.message.create({
data: {
content: notificationMessage,
type: 'TEXT',
senderId: context.user.id,
senderOrganizationId: currentUser.organization.id,
receiverOrganizationId: args.input.partnerId,
},
})
console.warn(`✅ Уведомление отправлено поставщику ${partner.name}`)
} catch (notificationError) {
console.error('❌ Ошибка отправки уведомления:', notificationError)
// Не прерываем выполнение, если уведомление не отправилось
}
// Получаем полные данные заказа с маршрутами для ответа
const completeOrder = await prisma.supplyOrder.findUnique({
where: { id: supplyOrder.id },
include: {
partner: true,
organization: true,
fulfillmentCenter: true,
logisticsPartner: true,
employee: true,
routes: {
include: {
logistics: true,
},
},
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
},
},
})
// Формируем сообщение в зависимости от роли организации
let successMessage = ''
if (organizationRole === 'SELLER') {
successMessage = `Заказ поставки товаров создан! Товары будут доставлены ${
fulfillmentCenterId ? 'на указанный фулфилмент-центр' : 'согласно настройкам'
}. Ожидайте подтверждения от поставщика.`
} else if (organizationRole === 'FULFILLMENT') {
successMessage =
'Заказ поставки товаров создан для вашего склада! Ожидайте подтверждения от поставщика и координации с логистикой.'
} else if (organizationRole === 'LOGIST') {
successMessage =
'Заказ поставки создан и подтвержден! Координируйте доставку товаров от поставщика на фулфилмент-склад.'
}
return {
success: true,
message: successMessage,
order: completeOrder,
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)
console.error('ДЕТАЛИ ОШИБКИ:', error instanceof Error ? error.message : String(error))
console.error('СТЕК ОШИБКИ:', error instanceof Error ? error.stack : 'No stack')
return {
success: false,
message: `Ошибка при создании заказа поставки: ${error instanceof Error ? error.message : String(error)}`,
}
}
},
// Создать товар
createProduct: async (
_: unknown,
args: {
input: {
name: string
article: string
description?: string
price: number
pricePerSet?: number
quantity: number
setQuantity?: number
ordered?: number
inTransit?: number
stock?: number
sold?: number
type?: 'PRODUCT' | 'CONSUMABLE'
categoryId?: string
brand?: string
color?: string
size?: string
weight?: number
dimensions?: string
material?: string
images?: string[]
mainImage?: string
isActive?: boolean
}
},
context: Context,
) => {
console.warn('🆕 CREATE_PRODUCT RESOLVER - ВЫЗВАН:', {
hasUser: !!context.user,
userId: context.user?.id,
inputData: args.input,
timestamp: new Date().toISOString(),
})
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что это поставщик
if (currentUser.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Товары доступны только для поставщиков')
}
// Проверяем уникальность артикула в рамках организации
const existingProduct = await prisma.product.findFirst({
where: {
article: args.input.article,
organizationId: currentUser.organization.id,
},
})
if (existingProduct) {
return {
success: false,
message: 'Товар с таким артикулом уже существует',
}
}
try {
console.warn('🛍️ СОЗДАНИЕ ТОВАРА - НАЧАЛО:', {
userId: currentUser.id,
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
productData: {
name: args.input.name,
article: args.input.article,
type: args.input.type || 'PRODUCT',
isActive: args.input.isActive ?? true,
},
})
const product = await prisma.product.create({
data: {
name: args.input.name,
article: args.input.article,
description: args.input.description,
price: args.input.price,
pricePerSet: args.input.pricePerSet,
quantity: args.input.quantity,
setQuantity: args.input.setQuantity,
ordered: args.input.ordered,
inTransit: args.input.inTransit,
stock: args.input.stock,
sold: args.input.sold,
type: args.input.type || 'PRODUCT',
categoryId: args.input.categoryId,
brand: args.input.brand,
color: args.input.color,
size: args.input.size,
weight: args.input.weight,
dimensions: args.input.dimensions,
material: args.input.material,
images: JSON.stringify(args.input.images || []),
mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true,
organizationId: currentUser.organization.id,
},
include: {
category: true,
organization: true,
},
})
console.warn('✅ ТОВАР УСПЕШНО СОЗДАН:', {
productId: product.id,
name: product.name,
article: product.article,
type: product.type,
isActive: product.isActive,
organizationId: product.organizationId,
createdAt: product.createdAt,
})
return {
success: true,
message: 'Товар успешно создан',
product,
}
} catch (error) {
console.error('Error creating product:', error)
return {
success: false,
message: 'Ошибка при создании товара',
}
}
},
// Обновить товар
updateProduct: async (
_: unknown,
args: {
id: string
input: {
name: string
article: string
description?: string
price: number
pricePerSet?: number
quantity: number
setQuantity?: number
ordered?: number
inTransit?: number
stock?: number
sold?: number
type?: 'PRODUCT' | 'CONSUMABLE'
categoryId?: string
brand?: string
color?: string
size?: string
weight?: number
dimensions?: string
material?: string
images?: string[]
mainImage?: string
isActive?: boolean
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что товар принадлежит текущей организации
const existingProduct = await prisma.product.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
})
if (!existingProduct) {
throw new GraphQLError('Товар не найден или нет доступа')
}
// Проверяем уникальность артикула (если он изменился)
if (args.input.article !== existingProduct.article) {
const duplicateProduct = await prisma.product.findFirst({
where: {
article: args.input.article,
organizationId: currentUser.organization.id,
NOT: { id: args.id },
},
})
if (duplicateProduct) {
return {
success: false,
message: 'Товар с таким артикулом уже существует',
}
}
}
try {
const product = await prisma.product.update({
where: { id: args.id },
data: {
name: args.input.name,
article: args.input.article,
description: args.input.description,
price: args.input.price,
pricePerSet: args.input.pricePerSet,
quantity: args.input.quantity,
setQuantity: args.input.setQuantity,
ordered: args.input.ordered,
inTransit: args.input.inTransit,
stock: args.input.stock,
sold: args.input.sold,
...(args.input.type && { type: args.input.type }),
categoryId: args.input.categoryId,
brand: args.input.brand,
color: args.input.color,
size: args.input.size,
weight: args.input.weight,
dimensions: args.input.dimensions,
material: args.input.material,
images: args.input.images ? JSON.stringify(args.input.images) : undefined,
mainImage: args.input.mainImage,
isActive: args.input.isActive ?? true,
},
include: {
category: true,
organization: true,
},
})
return {
success: true,
message: 'Товар успешно обновлен',
product,
}
} catch (error) {
console.error('Error updating product:', error)
return {
success: false,
message: 'Ошибка при обновлении товара',
}
}
},
// Проверка уникальности артикула
checkArticleUniqueness: async (_: unknown, args: { article: string; excludeId?: string }, context: Context) => {
const { currentUser, prisma } = context
if (!currentUser?.organization?.id) {
return {
isUnique: false,
existingProduct: null,
}
}
try {
const existingProduct = await prisma.product.findFirst({
where: {
article: args.article,
organizationId: currentUser.organization.id,
...(args.excludeId && { id: { not: args.excludeId } }),
},
select: {
id: true,
name: true,
article: true,
},
})
return {
isUnique: !existingProduct,
existingProduct,
}
} catch (error) {
console.error('Error checking article uniqueness:', error)
return {
isUnique: false,
existingProduct: null,
}
}
},
// Резервирование товара при создании заказа
reserveProductStock: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
const { currentUser, prisma } = context
if (!currentUser?.organization?.id) {
return {
success: false,
message: 'Необходимо авторизоваться',
}
}
try {
const product = await prisma.product.findUnique({
where: { id: args.productId },
})
if (!product) {
return {
success: false,
message: 'Товар не найден',
}
}
// Проверяем доступность товара
const availableStock = (product.stock || product.quantity) - (product.ordered || 0)
if (availableStock < args.quantity) {
return {
success: false,
message: `Недостаточно товара на складе. Доступно: ${availableStock}, запрошено: ${args.quantity}`,
}
}
// Резервируем товар (увеличиваем поле ordered)
const updatedProduct = await prisma.product.update({
where: { id: args.productId },
data: {
ordered: (product.ordered || 0) + args.quantity,
},
})
console.warn(`📦 Зарезервировано ${args.quantity} единиц товара ${product.name}`)
return {
success: true,
message: `Зарезервировано ${args.quantity} единиц товара`,
product: updatedProduct,
}
} catch (error) {
console.error('Error reserving product stock:', error)
return {
success: false,
message: 'Ошибка при резервировании товара',
}
}
},
// Освобождение резерва при отмене заказа
releaseProductReserve: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
const { currentUser, prisma } = context
if (!currentUser?.organization?.id) {
return {
success: false,
message: 'Необходимо авторизоваться',
}
}
try {
const product = await prisma.product.findUnique({
where: { id: args.productId },
})
if (!product) {
return {
success: false,
message: 'Товар не найден',
}
}
// Освобождаем резерв (уменьшаем поле ordered)
const newOrdered = Math.max((product.ordered || 0) - args.quantity, 0)
const updatedProduct = await prisma.product.update({
where: { id: args.productId },
data: {
ordered: newOrdered,
},
})
console.warn(`🔄 Освобожден резерв ${args.quantity} единиц товара ${product.name}`)
return {
success: true,
message: `Освобожден резерв ${args.quantity} единиц товара`,
product: updatedProduct,
}
} catch (error) {
console.error('Error releasing product reserve:', error)
return {
success: false,
message: 'Ошибка при освобождении резерва',
}
}
},
// Обновление статуса "в пути"
updateProductInTransit: async (
_: unknown,
args: { productId: string; quantity: number; operation: string },
context: Context,
) => {
const { currentUser, prisma } = context
if (!currentUser?.organization?.id) {
return {
success: false,
message: 'Необходимо авторизоваться',
}
}
try {
const product = await prisma.product.findUnique({
where: { id: args.productId },
})
if (!product) {
return {
success: false,
message: 'Товар не найден',
}
}
let newInTransit = product.inTransit || 0
let newOrdered = product.ordered || 0
if (args.operation === 'ship') {
// При отгрузке: переводим из "заказано" в "в пути"
newInTransit = (product.inTransit || 0) + args.quantity
newOrdered = Math.max((product.ordered || 0) - args.quantity, 0)
} else if (args.operation === 'deliver') {
// При доставке: убираем из "в пути", добавляем в "продано"
newInTransit = Math.max((product.inTransit || 0) - args.quantity, 0)
}
const updatedProduct = await prisma.product.update({
where: { id: args.productId },
data: {
inTransit: newInTransit,
ordered: newOrdered,
...(args.operation === 'deliver' && {
sold: (product.sold || 0) + args.quantity,
}),
},
})
console.warn(`🚚 Обновлен статус "в пути" для товара ${product.name}: ${args.operation}`)
return {
success: true,
message: `Статус товара обновлен: ${args.operation}`,
product: updatedProduct,
}
} catch (error) {
console.error('Error updating product in transit:', error)
return {
success: false,
message: 'Ошибка при обновлении статуса товара',
}
}
},
// Удалить товар
deleteProduct: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что товар принадлежит текущей организации
const existingProduct = await prisma.product.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
})
if (!existingProduct) {
throw new GraphQLError('Товар не найден или нет доступа')
}
try {
await prisma.product.delete({
where: { id: args.id },
})
return true
} catch (error) {
console.error('Error deleting product:', error)
return false
}
},
// Создать категорию
createCategory: async (_: unknown, args: { input: { name: string } }, context: Context) => {
if (!context.user && !context.admin) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
// Проверяем уникальность названия категории
const existingCategory = await prisma.category.findUnique({
where: { name: args.input.name },
})
if (existingCategory) {
return {
success: false,
message: 'Категория с таким названием уже существует',
}
}
try {
const category = await prisma.category.create({
data: {
name: args.input.name,
},
})
return {
success: true,
message: 'Категория успешно создана',
category,
}
} catch (error) {
console.error('Error creating category:', error)
return {
success: false,
message: 'Ошибка при создании категории',
}
}
},
// Обновить категорию
updateCategory: async (_: unknown, args: { id: string; input: { name: string } }, context: Context) => {
if (!context.user && !context.admin) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
// Проверяем существование категории
const existingCategory = await prisma.category.findUnique({
where: { id: args.id },
})
if (!existingCategory) {
return {
success: false,
message: 'Категория не найдена',
}
}
// Проверяем уникальность нового названия (если изменилось)
if (args.input.name !== existingCategory.name) {
const duplicateCategory = await prisma.category.findUnique({
where: { name: args.input.name },
})
if (duplicateCategory) {
return {
success: false,
message: 'Категория с таким названием уже существует',
}
}
}
try {
const category = await prisma.category.update({
where: { id: args.id },
data: {
name: args.input.name,
},
})
return {
success: true,
message: 'Категория успешно обновлена',
category,
}
} catch (error) {
console.error('Error updating category:', error)
return {
success: false,
message: 'Ошибка при обновлении категории',
}
}
},
// Удалить категорию
deleteCategory: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user && !context.admin) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
// Проверяем существование категории
const existingCategory = await prisma.category.findUnique({
where: { id: args.id },
include: { products: true },
})
if (!existingCategory) {
throw new GraphQLError('Категория не найдена')
}
// Проверяем, есть ли товары в этой категории
if (existingCategory.products.length > 0) {
throw new GraphQLError('Нельзя удалить категорию, в которой есть товары')
}
try {
await prisma.category.delete({
where: { id: args.id },
})
return true
} catch (error) {
console.error('Error deleting category:', error)
return false
}
},
// Добавить товар в корзину
addToCart: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что товар существует и активен
const product = await prisma.product.findFirst({
where: {
id: args.productId,
isActive: true,
},
include: {
organization: true,
},
})
if (!product) {
return {
success: false,
message: 'Товар не найден или неактивен',
}
}
// Проверяем, что пользователь не пытается добавить свой собственный товар
if (product.organizationId === currentUser.organization.id) {
return {
success: false,
message: 'Нельзя добавлять собственные товары в корзину',
}
}
// Найти или создать корзину
let cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id },
})
if (!cart) {
cart = await prisma.cart.create({
data: {
organizationId: currentUser.organization.id,
},
})
}
try {
// Проверяем, есть ли уже такой товар в корзине
const existingCartItem = await prisma.cartItem.findUnique({
where: {
cartId_productId: {
cartId: cart.id,
productId: args.productId,
},
},
})
if (existingCartItem) {
// Обновляем количество
const newQuantity = existingCartItem.quantity + args.quantity
if (newQuantity > product.quantity) {
return {
success: false,
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
}
}
await prisma.cartItem.update({
where: { id: existingCartItem.id },
data: { quantity: newQuantity },
})
} else {
// Создаем новый элемент корзины
if (args.quantity > product.quantity) {
return {
success: false,
message: `Недостаточно товара в наличии. Доступно: ${product.quantity}`,
}
}
await prisma.cartItem.create({
data: {
cartId: cart.id,
productId: args.productId,
quantity: args.quantity,
},
})
}
// Возвращаем обновленную корзину
const updatedCart = await prisma.cart.findUnique({
where: { id: cart.id },
include: {
items: {
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
},
},
},
organization: true,
},
})
return {
success: true,
message: 'Товар добавлен в корзину',
cart: updatedCart,
}
} catch (error) {
console.error('Error adding to cart:', error)
return {
success: false,
message: 'Ошибка при добавлении в корзину',
}
}
},
// Обновить количество товара в корзине
updateCartItem: async (_: unknown, args: { productId: string; quantity: number }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id },
})
if (!cart) {
return {
success: false,
message: 'Корзина не найдена',
}
}
// Проверяем, что товар существует в корзине
const cartItem = await prisma.cartItem.findUnique({
where: {
cartId_productId: {
cartId: cart.id,
productId: args.productId,
},
},
include: {
product: true,
},
})
if (!cartItem) {
return {
success: false,
message: 'Товар не найден в корзине',
}
}
if (args.quantity <= 0) {
return {
success: false,
message: 'Количество должно быть больше 0',
}
}
if (args.quantity > cartItem.product.quantity) {
return {
success: false,
message: `Недостаточно товара в наличии. Доступно: ${cartItem.product.quantity}`,
}
}
try {
await prisma.cartItem.update({
where: { id: cartItem.id },
data: { quantity: args.quantity },
})
// Возвращаем обновленную корзину
const updatedCart = await prisma.cart.findUnique({
where: { id: cart.id },
include: {
items: {
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
},
},
},
organization: true,
},
})
return {
success: true,
message: 'Количество товара обновлено',
cart: updatedCart,
}
} catch (error) {
console.error('Error updating cart item:', error)
return {
success: false,
message: 'Ошибка при обновлении корзины',
}
}
},
// Удалить товар из корзины
removeFromCart: async (_: unknown, args: { productId: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id },
})
if (!cart) {
return {
success: false,
message: 'Корзина не найдена',
}
}
try {
await prisma.cartItem.delete({
where: {
cartId_productId: {
cartId: cart.id,
productId: args.productId,
},
},
})
// Возвращаем обновленную корзину
const updatedCart = await prisma.cart.findUnique({
where: { id: cart.id },
include: {
items: {
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
},
},
},
organization: true,
},
})
return {
success: true,
message: 'Товар удален из корзины',
cart: updatedCart,
}
} catch (error) {
console.error('Error removing from cart:', error)
return {
success: false,
message: 'Ошибка при удалении из корзины',
}
}
},
// Очистить корзину
clearCart: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
const cart = await prisma.cart.findUnique({
where: { organizationId: currentUser.organization.id },
})
if (!cart) {
return false
}
try {
await prisma.cartItem.deleteMany({
where: { cartId: cart.id },
})
return true
} catch (error) {
console.error('Error clearing cart:', error)
return false
}
},
// Добавить товар в избранное
addToFavorites: async (_: unknown, args: { productId: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что товар существует и активен
const product = await prisma.product.findFirst({
where: {
id: args.productId,
isActive: true,
},
include: {
organization: true,
},
})
if (!product) {
return {
success: false,
message: 'Товар не найден или неактивен',
}
}
// Проверяем, что пользователь не пытается добавить свой собственный товар
if (product.organizationId === currentUser.organization.id) {
return {
success: false,
message: 'Нельзя добавлять собственные товары в избранное',
}
}
try {
// Проверяем, есть ли уже такой товар в избранном
const existingFavorite = await prisma.favorites.findUnique({
where: {
organizationId_productId: {
organizationId: currentUser.organization.id,
productId: args.productId,
},
},
})
if (existingFavorite) {
return {
success: false,
message: 'Товар уже в избранном',
}
}
// Добавляем товар в избранное
await prisma.favorites.create({
data: {
organizationId: currentUser.organization.id,
productId: args.productId,
},
})
// Возвращаем обновленный список избранного
const favorites = await prisma.favorites.findMany({
where: { organizationId: currentUser.organization.id },
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
},
},
orderBy: { createdAt: 'desc' },
})
return {
success: true,
message: 'Товар добавлен в избранное',
favorites: favorites.map((favorite) => favorite.product),
}
} catch (error) {
console.error('Error adding to favorites:', error)
return {
success: false,
message: 'Ошибка при добавлении в избранное',
}
}
},
// Удалить товар из избранного
removeFromFavorites: async (_: unknown, args: { productId: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
// Удаляем товар из избранного
await prisma.favorites.deleteMany({
where: {
organizationId: currentUser.organization.id,
productId: args.productId,
},
})
// Возвращаем обновленный список избранного
const favorites = await prisma.favorites.findMany({
where: { organizationId: currentUser.organization.id },
include: {
product: {
include: {
category: true,
organization: {
include: {
users: true,
},
},
},
},
},
orderBy: { createdAt: 'desc' },
})
return {
success: true,
message: 'Товар удален из избранного',
favorites: favorites.map((favorite) => favorite.product),
}
} catch (error) {
console.error('Error removing from favorites:', error)
return {
success: false,
message: 'Ошибка при удалении из избранного',
}
}
},
// Создать сотрудника
createEmployee: async (_: unknown, args: { input: CreateEmployeeInput }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
}
try {
const employee = await prisma.employee.create({
data: {
...args.input,
organizationId: currentUser.organization.id,
birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined,
passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined,
hireDate: new Date(args.input.hireDate),
},
include: {
organization: true,
},
})
return {
success: true,
message: 'Сотрудник успешно добавлен',
employee,
}
} catch (error) {
console.error('Error creating employee:', error)
return {
success: false,
message: 'Ошибка при создании сотрудника',
}
}
},
// Обновить сотрудника
updateEmployee: async (_: unknown, args: { id: string; input: UpdateEmployeeInput }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
}
try {
const employee = await prisma.employee.update({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
data: {
...args.input,
birthDate: args.input.birthDate ? new Date(args.input.birthDate) : undefined,
passportDate: args.input.passportDate ? new Date(args.input.passportDate) : undefined,
hireDate: args.input.hireDate ? new Date(args.input.hireDate) : undefined,
},
include: {
organization: true,
},
})
return {
success: true,
message: 'Сотрудник успешно обновлен',
employee,
}
} catch (error) {
console.error('Error updating employee:', error)
return {
success: false,
message: 'Ошибка при обновлении сотрудника',
}
}
},
// Удалить сотрудника
deleteEmployee: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
}
try {
await prisma.employee.delete({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
})
return true
} catch (error) {
console.error('Error deleting employee:', error)
return false
}
},
// Обновить табель сотрудника
updateEmployeeSchedule: async (_: unknown, args: { input: UpdateScheduleInput }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
}
try {
// Проверяем что сотрудник принадлежит организации
const employee = await prisma.employee.findFirst({
where: {
id: args.input.employeeId,
organizationId: currentUser.organization.id,
},
})
if (!employee) {
throw new GraphQLError('Сотрудник не найден')
}
// Создаем или обновляем запись табеля
await prisma.employeeSchedule.upsert({
where: {
employeeId_date: {
employeeId: args.input.employeeId,
date: new Date(args.input.date),
},
},
create: {
employeeId: args.input.employeeId,
date: new Date(args.input.date),
status: args.input.status,
hoursWorked: args.input.hoursWorked,
overtimeHours: args.input.overtimeHours,
notes: args.input.notes,
},
update: {
status: args.input.status,
hoursWorked: args.input.hoursWorked,
overtimeHours: args.input.overtimeHours,
notes: args.input.notes,
},
})
return true
} catch (error) {
console.error('Error updating employee schedule:', error)
return false
}
},
// Создать поставку Wildberries
createWildberriesSupply: async (
_: unknown,
args: {
input: {
cards: Array<{
price: number
discountedPrice?: number
selectedQuantity: number
selectedServices?: string[]
}>
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
// Пока что просто логируем данные, так как таблицы еще нет
console.warn('Создание поставки Wildberries с данными:', args.input)
const totalAmount = args.input.cards.reduce((sum: number, card) => {
const cardPrice = card.discountedPrice || card.price
const servicesPrice = (card.selectedServices?.length || 0) * 50
return sum + (cardPrice + servicesPrice) * card.selectedQuantity
}, 0)
const totalItems = args.input.cards.reduce((sum: number, card) => sum + card.selectedQuantity, 0)
// Временная заглушка - вернем success без создания в БД
return {
success: true,
message: `Поставка создана успешно! Товаров: ${totalItems}, Сумма: ${totalAmount} руб.`,
supply: null, // Временно null
}
} catch (error) {
console.error('Error creating Wildberries supply:', error)
return {
success: false,
message: 'Ошибка при создании поставки Wildberries',
}
}
},
// Создать поставщика для поставки
createSupplySupplier: async (
_: unknown,
args: {
input: {
name: string
contactName: string
phone: string
market?: string
address?: string
place?: string
telegram?: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
// Создаем поставщика в базе данных
const supplier = await prisma.supplySupplier.create({
data: {
name: args.input.name,
contactName: args.input.contactName,
phone: args.input.phone,
market: args.input.market,
address: args.input.address,
place: args.input.place,
telegram: args.input.telegram,
organizationId: currentUser.organization.id,
},
})
return {
success: true,
message: 'Поставщик добавлен успешно!',
supplier: {
id: supplier.id,
name: supplier.name,
contactName: supplier.contactName,
phone: supplier.phone,
market: supplier.market,
address: supplier.address,
place: supplier.place,
telegram: supplier.telegram,
createdAt: supplier.createdAt,
},
}
} catch (error) {
console.error('Error creating supply supplier:', error)
return {
success: false,
message: 'Ошибка при добавлении поставщика',
}
}
},
// Обновить статус заказа поставки
updateSupplyOrderStatus: async (
_: unknown,
args: {
id: string
status:
| 'PENDING'
| 'CONFIRMED'
| 'IN_TRANSIT'
| 'SUPPLIER_APPROVED'
| 'LOGISTICS_CONFIRMED'
| 'SHIPPED'
| 'DELIVERED'
| 'CANCELLED'
},
context: Context,
) => {
console.warn(`[DEBUG] updateSupplyOrderStatus вызван для заказа ${args.id} со статусом ${args.status}`)
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
// Находим заказ поставки
const existingOrder = await prisma.supplyOrder.findFirst({
where: {
id: args.id,
OR: [
{ organizationId: currentUser.organization.id }, // Создатель заказа
{ partnerId: currentUser.organization.id }, // Поставщик
{ fulfillmentCenterId: currentUser.organization.id }, // Фулфилмент-центр
],
},
include: {
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
partner: true,
fulfillmentCenter: true,
},
})
if (!existingOrder) {
throw new GraphQLError('Заказ поставки не найден или нет доступа')
}
// Обновляем статус заказа
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.id },
data: { status: args.status },
include: {
partner: true,
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
},
})
// ОТКЛЮЧЕНО: Устаревшая логика для обновления расходников
// Теперь используются специальные мутации для каждой роли
const targetOrganizationId = existingOrder.fulfillmentCenterId || existingOrder.organizationId
if (args.status === 'CONFIRMED') {
console.warn(`[WARNING] Попытка использовать устаревший статус CONFIRMED для заказа ${args.id}`)
// Не обновляем расходники для устаревших статусов
// await prisma.supply.updateMany({
// where: {
// organizationId: targetOrganizationId,
// status: "planned",
// name: {
// in: existingOrder.items.map(item => item.product.name)
// }
// },
// data: {
// status: "confirmed"
// }
// });
console.warn("✅ Статусы расходников обновлены на 'confirmed'")
}
if (args.status === 'IN_TRANSIT') {
// При отгрузке - переводим расходники в статус "in-transit"
await prisma.supply.updateMany({
where: {
organizationId: targetOrganizationId,
status: 'confirmed',
name: {
in: existingOrder.items.map((item) => item.product.name),
},
},
data: {
status: 'in-transit',
},
})
console.warn("✅ Статусы расходников обновлены на 'in-transit'")
}
// Если статус изменился на DELIVERED, обновляем склад
if (args.status === 'DELIVERED') {
console.warn('🚚 Обновляем склад организации:', {
targetOrganizationId,
fulfillmentCenterId: existingOrder.fulfillmentCenterId,
organizationId: existingOrder.organizationId,
itemsCount: existingOrder.items.length,
items: existingOrder.items.map((item) => ({
productName: item.product.name,
quantity: item.quantity,
})),
})
// 🔄 СИНХРОНИЗАЦИЯ: Обновляем товары поставщика (переводим из "в пути" в "продано" + обновляем основные остатки)
for (const item of existingOrder.items) {
const product = await prisma.product.findUnique({
where: { id: item.product.id },
})
if (product) {
// ИСПРАВЛЕНО: НЕ списываем повторно, только переводим из inTransit в sold
// Остаток уже был уменьшен при создании/одобрении заказа
await prisma.product.update({
where: { id: item.product.id },
data: {
// НЕ ТРОГАЕМ stock - он уже правильно уменьшен при заказе
// Только переводим из inTransit в sold
inTransit: Math.max((product.inTransit || 0) - item.quantity, 0),
sold: (product.sold || 0) + item.quantity,
},
})
console.warn(
`✅ Товар поставщика "${product.name}" обновлен: доставлено ${
item.quantity
} единиц (остаток НЕ ИЗМЕНЕН: ${product.stock || product.quantity || 0})`,
)
}
}
// V2 СИСТЕМА: Расходники автоматически обрабатываются в seller-consumables.ts и fulfillment-consumables.ts
console.warn('📦 V2 система автоматически обработает инвентарь через специализированные резолверы')
console.warn('🎉 Склад организации успешно обновлен!')
}
// Уведомляем вовлеченные организации об изменении статуса заказа
try {
const orgIds = [
existingOrder.organizationId,
existingOrder.partnerId,
existingOrder.fulfillmentCenterId || undefined,
].filter(Boolean) as string[]
notifyMany(orgIds, {
type: 'supply-order:updated',
payload: { id: updatedOrder.id, status: updatedOrder.status },
})
} catch {}
return {
success: true,
message: `Статус заказа поставки обновлен на "${args.status}"`,
order: updatedOrder,
}
} catch (error) {
console.error('Error updating supply order status:', error)
return {
success: false,
message: 'Ошибка при обновлении статуса заказа поставки',
}
}
},
// Обновление параметров поставки (объём и грузовые места)
updateSupplyParameters: async (
_: unknown,
args: { id: string; volume?: number; packagesCount?: number },
context: GraphQLContext,
) => {
try {
// Проверка аутентификации
if (!context.user?.id) {
return {
success: false,
message: 'Необходима аутентификация',
}
}
// Найти поставку и проверить права доступа
const supply = await prisma.supplyOrder.findUnique({
where: { id: args.id },
include: { partner: true },
})
if (!supply) {
return {
success: false,
message: 'Поставка не найдена',
}
}
// Проверить, что пользователь - поставщик этой заявки
if (supply.partnerId !== context.user.organization?.id) {
return {
success: false,
message: 'Недостаточно прав для изменения данной поставки',
}
}
// Подготовить данные для обновления
const updateData: { volume?: number; packagesCount?: number } = {}
if (args.volume !== undefined) updateData.volume = args.volume
if (args.packagesCount !== undefined) updateData.packagesCount = args.packagesCount
// Обновить поставку
const updatedSupply = await prisma.supplyOrder.update({
where: { id: args.id },
data: updateData,
})
return {
success: true,
message: 'Параметры поставки обновлены',
order: updatedSupply,
}
} catch (error) {
console.error('Ошибка при обновлении параметров поставки:', error)
return {
success: false,
message: 'Ошибка при обновлении параметров поставки',
}
}
},
// Назначение логистики фулфилментом на заказ селлера
assignLogisticsToSupply: async (
_: unknown,
args: {
supplyOrderId: string
logisticsPartnerId: string
responsibleId?: string
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что пользователь - фулфилмент
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент может назначать логистику')
}
try {
// Находим заказ
const existingOrder = await prisma.supplyOrder.findUnique({
where: { id: args.supplyOrderId },
include: {
partner: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: { product: true },
},
},
})
if (!existingOrder) {
throw new GraphQLError('Заказ поставки не найден')
}
// Проверяем, что это заказ для нашего фулфилмент-центра
if (existingOrder.fulfillmentCenterId !== currentUser.organization.id) {
throw new GraphQLError('Нет доступа к этому заказу')
}
// Проверяем, что статус позволяет назначить логистику
if (existingOrder.status !== 'SUPPLIER_APPROVED') {
throw new GraphQLError(`Нельзя назначить логистику для заказа со статусом ${existingOrder.status}`)
}
// Проверяем, что логистическая компания существует
const logisticsPartner = await prisma.organization.findUnique({
where: { id: args.logisticsPartnerId },
})
if (!logisticsPartner || logisticsPartner.type !== 'LOGIST') {
throw new GraphQLError('Логистическая компания не найдена')
}
// Обновляем заказ
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.supplyOrderId },
data: {
logisticsPartner: {
connect: { id: args.logisticsPartnerId },
},
status: 'CONFIRMED', // Переводим в статус "подтвержден фулфилментом"
},
include: {
partner: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: { product: true },
},
},
})
console.warn(`✅ Логистика назначена на заказ ${args.supplyOrderId}:`, {
logisticsPartner: logisticsPartner.name,
responsible: args.responsibleId,
newStatus: 'CONFIRMED',
})
try {
const orgIds = [
existingOrder.organizationId,
existingOrder.partnerId,
existingOrder.fulfillmentCenterId || undefined,
args.logisticsPartnerId,
].filter(Boolean) as string[]
notifyMany(orgIds, {
type: 'supply-order:updated',
payload: { id: updatedOrder.id, status: updatedOrder.status },
})
} catch {}
return {
success: true,
message: 'Логистика успешно назначена',
order: updatedOrder,
}
} catch (error) {
console.error('❌ Ошибка при назначении логистики:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка при назначении логистики',
}
}
},
// 🔒 МУТАЦИИ ПОСТАВЩИКА С СИСТЕМОЙ БЕЗОПАСНОСТИ
supplierApproveOrder: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА
if (currentUser.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Доступ разрешен только поставщикам (WHOLESALE)')
}
try {
// 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ
const securityContext: SecurityContext = {
userId: currentUser.id,
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
userRole: currentUser.organization.type,
requestMetadata: {
action: 'APPROVE_ORDER',
resourceId: args.id,
timestamp: new Date().toISOString(),
ipAddress: context.req?.ip || 'unknown',
userAgent: context.req?.get('user-agent') || 'unknown',
},
}
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
await ParticipantIsolation.validateAccess(
prisma,
currentUser.organization.id,
currentUser.organization.type,
'SUPPLY_ORDER',
)
// 🔒 ПОЛУЧЕНИЕ ЗАКАЗА С ПРОВЕРКОЙ ДОСТУПА
const existingOrder = await prisma.supplyOrder.findFirst({
where: {
id: args.id,
partnerId: currentUser.organization.id, // Только поставщик может одобрить
status: 'PENDING', // Можно одобрить только заказы в статусе PENDING
},
include: {
organization: true,
partner: true,
},
})
if (!existingOrder) {
return {
success: false,
message: 'Заказ не найден или недоступен для одобрения',
}
}
// 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ
await ParticipantIsolation.validatePartnerAccess(
prisma,
currentUser.organization.id,
existingOrder.organizationId,
)
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
await CommercialDataAudit.logAccess(prisma, {
userId: currentUser.id,
organizationType: currentUser.organization.type,
action: 'APPROVE_ORDER',
resourceType: 'SUPPLY_ORDER',
resourceId: args.id,
metadata: {
partnerOrganizationId: existingOrder.organizationId,
orderValue: existingOrder.totalAmount?.toString() || '0',
...securityContext.requestMetadata,
},
})
console.warn(`[DEBUG] Поставщик ${currentUser.organization.name} одобряет заказ ${args.id}`)
// 🔄 СИНХРОНИЗАЦИЯ ОСТАТКОВ: Резервируем товары у поставщика
const orderWithItems = await prisma.supplyOrder.findUnique({
where: { id: args.id },
include: {
items: {
include: {
product: true,
},
},
},
})
if (orderWithItems) {
for (const item of orderWithItems.items) {
// Резервируем товар (увеличиваем поле ordered)
const product = await prisma.product.findUnique({
where: { id: item.product.id },
})
if (product) {
const availableStock = (product.stock || product.quantity) - (product.ordered || 0)
if (availableStock < item.quantity) {
return {
success: false,
message: `Недостаточно товара "${product.name}" на складе. Доступно: ${availableStock}, требуется: ${item.quantity}`,
}
}
// Согласно правилам: при одобрении заказа остаток должен уменьшиться
const currentStock = product.stock || product.quantity || 0
const newStock = Math.max(currentStock - item.quantity, 0)
await prisma.product.update({
where: { id: item.product.id },
data: {
// Уменьшаем основной остаток (товар зарезервирован для заказа)
stock: newStock,
quantity: newStock, // Синхронизируем оба поля для совместимости
// Увеличиваем количество заказанного (для отслеживания)
ordered: (product.ordered || 0) + item.quantity,
},
})
console.warn(`📦 Товар "${product.name}" зарезервирован: ${item.quantity} единиц`)
console.warn(` 📊 Остаток: ${currentStock} -> ${newStock} (уменьшен на ${item.quantity})`)
console.warn(` 📋 Заказано: ${product.ordered || 0} -> ${(product.ordered || 0) + item.quantity}`)
}
}
}
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.id },
data: { status: 'SUPPLIER_APPROVED' },
include: {
partner: true,
organization: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
},
},
})
console.warn('[DEBUG] updatedOrder structure:', {
id: updatedOrder.id,
itemsCount: updatedOrder.items?.length || 0,
firstItem: updatedOrder.items?.[0] ? {
productId: updatedOrder.items[0].productId,
hasProduct: !!updatedOrder.items[0].product,
productOrgId: updatedOrder.items[0].product?.organizationId,
hasProductOrg: !!updatedOrder.items[0].product?.organization,
} : null,
currentUserOrgId: currentUser.organization.id,
})
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser)
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType)
console.warn(`[DEBUG] Заказ ${args.id} успешно обновлен до статуса: ${updatedOrder.status}`)
console.warn('[DEBUG] filteredOrder:', {
hasData: !!filteredOrder.data,
dataId: filteredOrder.data?.id,
dataKeys: Object.keys(filteredOrder.data || {}),
})
try {
const orgIds = [
updatedOrder.organizationId,
updatedOrder.partnerId,
updatedOrder.fulfillmentCenterId || undefined,
updatedOrder.logisticsPartnerId || undefined,
].filter(Boolean) as string[]
notifyMany(orgIds, {
type: 'supply-order:updated',
payload: { id: updatedOrder.id, status: updatedOrder.status },
})
} catch {}
// Проверка на случай, если фильтрованные данные null
if (!filteredOrder.data || !filteredOrder.data.id) {
console.error('[ERROR] filteredOrder.data is null or missing id:', filteredOrder)
throw new GraphQLError('Filtered order data is invalid')
}
return {
success: true,
message: 'Заказ поставки одобрен поставщиком. Товары зарезервированы, остатки обновлены.',
order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data)
}
} 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('У пользователя нет организации')
}
// 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА
if (currentUser.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Доступ разрешен только поставщикам (WHOLESALE)')
}
try {
// 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ
const securityContext: SecurityContext = {
userId: currentUser.id,
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
userRole: currentUser.organization.type,
requestMetadata: {
action: 'REJECT_ORDER',
resourceId: args.id,
timestamp: new Date().toISOString(),
ipAddress: context.req?.ip || 'unknown',
userAgent: context.req?.get('user-agent') || 'unknown',
},
}
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
await ParticipantIsolation.validateAccess(
prisma,
currentUser.organization.id,
currentUser.organization.type,
'SUPPLY_ORDER',
)
// 🔒 ПОЛУЧЕНИЕ ЗАКАЗА С ПРОВЕРКОЙ ДОСТУПА
const existingOrder = await prisma.supplyOrder.findFirst({
where: {
id: args.id,
partnerId: currentUser.organization.id,
status: 'PENDING',
},
include: {
organization: true,
partner: true,
},
})
if (!existingOrder) {
return {
success: false,
message: 'Заказ не найден или недоступен для отклонения',
}
}
// 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ
await ParticipantIsolation.validatePartnerAccess(
prisma,
currentUser.organization.id,
existingOrder.organizationId,
)
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
await CommercialDataAudit.logAccess(prisma, {
userId: currentUser.id,
organizationType: currentUser.organization.type,
action: 'REJECT_ORDER',
resourceType: 'SUPPLY_ORDER',
resourceId: args.id,
metadata: {
partnerOrganizationId: existingOrder.organizationId,
orderValue: existingOrder.totalAmount?.toString() || '0',
rejectionReason: args.reason,
...securityContext.requestMetadata,
},
})
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,
},
},
},
},
},
})
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser)
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType)
// 📦 СНИМАЕМ РЕЗЕРВАЦИЮ ПРИ ОТКЛОНЕНИИ
// Восстанавливаем остатки и убираем резервацию для каждого отклоненного товара
for (const item of updatedOrder.items) {
const product = await prisma.product.findUnique({
where: { id: item.productId },
})
if (product) {
// Восстанавливаем основные остатки (на случай, если заказ был одобрен, а затем отклонен)
const currentStock = product.stock || product.quantity || 0
const restoredStock = currentStock + item.quantity
await prisma.product.update({
where: { id: item.productId },
data: {
// Восстанавливаем основной остаток
stock: restoredStock,
quantity: restoredStock,
// Уменьшаем количество заказанного
ordered: Math.max((product.ordered || 0) - item.quantity, 0),
},
})
console.warn(
`🔄 Восстановлены остатки товара "${product.name}": ${currentStock} -> ${restoredStock}, ordered: ${
product.ordered
} -> ${Math.max((product.ordered || 0) - item.quantity, 0)}`,
)
}
}
console.warn(
`📦 Снята резервация при отклонении заказа ${updatedOrder.id}:`,
updatedOrder.items.map((item) => `${item.productId}: -${item.quantity} шт.`).join(', '),
)
try {
const orgIds = [
updatedOrder.organizationId,
updatedOrder.partnerId,
updatedOrder.fulfillmentCenterId || undefined,
updatedOrder.logisticsPartnerId || undefined,
].filter(Boolean) as string[]
notifyMany(orgIds, {
type: 'supply-order:updated',
payload: { id: updatedOrder.id, status: updatedOrder.status },
})
} catch {}
return {
success: true,
message: args.reason ? `Заказ отклонен поставщиком. Причина: ${args.reason}` : 'Заказ отклонен поставщиком',
order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data)
}
} 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('У пользователя нет организации')
}
// 🔒 ПРОВЕРКА РОЛИ ПОСТАВЩИКА
if (currentUser.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Доступ разрешен только поставщикам (WHOLESALE)')
}
try {
// 🔒 СОЗДАНИЕ КОНТЕКСТА БЕЗОПАСНОСТИ
const securityContext: SecurityContext = {
userId: currentUser.id,
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
userRole: currentUser.organization.type,
requestMetadata: {
action: 'SHIP_ORDER',
resourceId: args.id,
timestamp: new Date().toISOString(),
ipAddress: context.req?.ip || 'unknown',
userAgent: context.req?.get('user-agent') || 'unknown',
},
}
// 🔒 ПРОВЕРКА ИЗОЛЯЦИИ УЧАСТНИКОВ
await ParticipantIsolation.validateAccess(
prisma,
currentUser.organization.id,
currentUser.organization.type,
'SUPPLY_ORDER',
)
// 🔒 ПОЛУЧЕНИЕ ЗАКАЗА С ПРОВЕРКОЙ ДОСТУПА
const existingOrder = await prisma.supplyOrder.findFirst({
where: {
id: args.id,
partnerId: currentUser.organization.id,
status: 'LOGISTICS_CONFIRMED',
},
include: {
organization: true,
partner: true,
},
})
if (!existingOrder) {
return {
success: false,
message: 'Заказ не найден или недоступен для отправки',
}
}
// 🔒 ПРОВЕРКА ПАРТНЕРСКИХ ОТНОШЕНИЙ
await ParticipantIsolation.validatePartnerAccess(
prisma,
currentUser.organization.id,
existingOrder.organizationId,
)
// 🔒 АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ
await CommercialDataAudit.logAccess(prisma, {
userId: currentUser.id,
organizationType: currentUser.organization.type,
action: 'SHIP_ORDER',
resourceType: 'SUPPLY_ORDER',
resourceId: args.id,
metadata: {
partnerOrganizationId: existingOrder.organizationId,
orderValue: existingOrder.totalAmount?.toString() || '0',
...securityContext.requestMetadata,
},
})
// 🔄 СИНХРОНИЗАЦИЯ ОСТАТКОВ: Переводим товары из "заказано" в "в пути"
const orderWithItems = await prisma.supplyOrder.findUnique({
where: { id: args.id },
include: {
items: {
include: {
product: true,
},
},
},
})
if (orderWithItems) {
for (const item of orderWithItems.items) {
const product = await prisma.product.findUnique({
where: { id: item.product.id },
})
if (product) {
await prisma.product.update({
where: { id: item.product.id },
data: {
ordered: Math.max((product.ordered || 0) - item.quantity, 0),
inTransit: (product.inTransit || 0) + item.quantity,
},
})
console.warn(`🚚 Товар "${product.name}" переведен в статус "в пути": ${item.quantity} единиц`)
}
}
}
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.id },
data: { status: 'SHIPPED' },
include: {
partner: true,
organization: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
recipe: {
include: {
services: true,
fulfillmentConsumables: true,
sellerConsumables: true,
},
},
},
},
},
})
// 🔒 ФИЛЬТРАЦИЯ ДАННЫХ ДЛЯ ПОСТАВЩИКА
const securityContextWithOrgType = createSecureContextWithOrgData(context, currentUser)
const filteredOrder = SupplyDataFilter.filterSupplyOrder(updatedOrder, securityContextWithOrgType)
try {
const orgIds = [
updatedOrder.organizationId,
updatedOrder.partnerId,
updatedOrder.fulfillmentCenterId || undefined,
updatedOrder.logisticsPartnerId || undefined,
].filter(Boolean) as string[]
notifyMany(orgIds, {
type: 'supply-order:updated',
payload: { id: updatedOrder.id, status: updatedOrder.status },
})
} catch {}
return {
success: true,
message: "Заказ отправлен поставщиком. Товары переведены в статус 'в пути'.",
order: filteredOrder.data, // 🔒 Возвращаем отфильтрованные данные (только data)
}
} catch (error) {
console.error('Error shipping supply order:', error)
return {
success: false,
message: 'Ошибка при отправке заказа поставки',
}
}
},
logisticsConfirmOrder: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
const existingOrder = await prisma.supplyOrder.findFirst({
where: {
id: args.id,
logisticsPartnerId: currentUser.organization.id,
OR: [{ status: 'SUPPLIER_APPROVED' }, { status: 'CONFIRMED' }],
},
})
if (!existingOrder) {
return {
success: false,
message: 'Заказ не найден или недоступен для подтверждения логистикой',
}
}
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.id },
data: { status: 'LOGISTICS_CONFIRMED' },
include: {
partner: true,
organization: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
},
},
})
try {
const orgIds = [
updatedOrder.organizationId,
updatedOrder.partnerId,
updatedOrder.fulfillmentCenterId || undefined,
updatedOrder.logisticsPartnerId || undefined,
].filter(Boolean) as string[]
notifyMany(orgIds, {
type: 'supply-order:updated',
payload: { id: updatedOrder.id, status: updatedOrder.status },
})
} catch {}
return {
success: true,
message: 'Заказ подтвержден логистической компанией',
order: updatedOrder,
}
} catch (error) {
console.error('Error confirming supply order:', error)
return {
success: false,
message: 'Ошибка при подтверждении заказа логистикой',
}
}
},
logisticsRejectOrder: async (_: unknown, args: { id: string; reason?: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
const existingOrder = await prisma.supplyOrder.findFirst({
where: {
id: args.id,
logisticsPartnerId: currentUser.organization.id,
OR: [{ status: 'SUPPLIER_APPROVED' }, { status: 'CONFIRMED' }],
},
})
if (!existingOrder) {
return {
success: false,
message: 'Заказ не найден или недоступен для отклонения логистикой',
}
}
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.id },
data: { status: 'CANCELLED' },
include: {
partner: true,
organization: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
},
},
})
try {
const orgIds = [
updatedOrder.organizationId,
updatedOrder.partnerId,
updatedOrder.fulfillmentCenterId || undefined,
updatedOrder.logisticsPartnerId || undefined,
].filter(Boolean) as string[]
notifyMany(orgIds, {
type: 'supply-order:updated',
payload: { id: updatedOrder.id, status: updatedOrder.status },
})
} catch {}
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 sellerSupply = await prisma.sellerConsumableSupplyOrder.findFirst({
where: {
id: args.id,
fulfillmentCenterId: currentUser.organization.id,
status: 'SHIPPED',
},
include: {
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
seller: true, // Селлер-создатель заказа
supplier: true, // Поставщик
fulfillmentCenter: true,
},
})
// Если нашли селлерскую поставку, обрабатываем её
if (sellerSupply) {
// Обновляем статус селлерской поставки
const updatedSellerOrder = await prisma.sellerConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'DELIVERED',
deliveredAt: new Date(),
},
include: {
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
seller: true,
supplier: true,
fulfillmentCenter: true,
},
})
// V2 СИСТЕМА: Инвентарь селлера автоматически обновляется через SellerConsumableInventory
console.warn('📦 V2 система автоматически обновит SellerConsumableInventory через processSellerConsumableSupplyReceipt')
return {
success: true,
message: 'Селлерская поставка принята фулфилментом. Расходники добавлены на склад.',
order: {
id: updatedSellerOrder.id,
status: updatedSellerOrder.status,
deliveryDate: updatedSellerOrder.requestedDeliveryDate,
totalAmount: updatedSellerOrder.items.reduce((sum, item) => sum + (item.unitPrice * item.requestedQuantity), 0),
totalItems: updatedSellerOrder.items.reduce((sum, item) => sum + item.requestedQuantity, 0),
partner: updatedSellerOrder.supplier,
organization: updatedSellerOrder.seller,
fulfillmentCenter: updatedSellerOrder.fulfillmentCenter,
},
}
}
// Если селлерской поставки нет, ищем старую поставку
const existingOrder = await prisma.supplyOrder.findFirst({
where: {
id: args.id,
fulfillmentCenterId: currentUser.organization.id,
status: 'SHIPPED',
},
include: {
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
organization: true, // Селлер-создатель заказа
partner: true, // Поставщик
},
})
if (!existingOrder) {
return {
success: false,
message: 'Заказ не найден или недоступен для приема',
}
}
// Обновляем статус заказа
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.id },
data: { status: 'DELIVERED' },
include: {
partner: true,
organization: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
},
},
})
// 🔄 СИНХРОНИЗАЦИЯ СКЛАДА ПОСТАВЩИКА: Обновляем остатки поставщика согласно правилам
console.warn('🔄 Начинаем синхронизацию остатков поставщика...')
for (const item of existingOrder.items) {
const product = await prisma.product.findUnique({
where: { id: item.product.id },
})
if (product) {
// ИСПРАВЛЕНО: НЕ списываем повторно, только переводим из inTransit в sold
// Остаток уже был уменьшен при создании/одобрении заказа
await prisma.product.update({
where: { id: item.product.id },
data: {
// НЕ ТРОГАЕМ stock - он уже правильно уменьшен при заказе
// Только переводим из inTransit в sold
inTransit: Math.max((product.inTransit || 0) - item.quantity, 0),
sold: (product.sold || 0) + item.quantity,
},
})
console.warn(`✅ Товар поставщика "${product.name}" обновлен: получено ${item.quantity} единиц`)
console.warn(
` 📊 Остаток: ${product.stock || product.quantity || 0} (НЕ ИЗМЕНЕН - уже списан при заказе)`,
)
console.warn(
` 🚚 В пути: ${product.inTransit || 0} -> ${Math.max(
(product.inTransit || 0) - item.quantity,
0,
)} (УБЫЛО: ${item.quantity})`,
)
console.warn(
` 💰 Продано: ${product.sold || 0} -> ${
(product.sold || 0) + item.quantity
} (ПРИБЫЛО: ${item.quantity})`,
)
}
}
// V2 СИСТЕМА: Инвентарь автоматически обновляется через специализированные резолверы
console.warn('📦 V2 система: склад обновится автоматически через FulfillmentConsumableInventory и SellerConsumableInventory')
console.warn('🎉 Синхронизация склада завершена успешно!')
return {
success: true,
message: 'Заказ принят фулфилментом. Склад обновлен. Остатки поставщика синхронизированы.',
order: updatedOrder,
}
} catch (error) {
console.error('Error receiving supply order:', error)
return {
success: false,
message: 'Ошибка при приеме заказа поставки',
}
}
},
updateExternalAdClicks: async (_: unknown, { id, clicks }: { id: string; clicks: number }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
// Проверяем, что реклама принадлежит организации пользователя
const existingAd = await prisma.externalAd.findFirst({
where: {
id,
organizationId: user.organization.id,
},
})
if (!existingAd) {
throw new GraphQLError('Внешняя реклама не найдена')
}
await prisma.externalAd.update({
where: { id },
data: { clicks },
})
return {
success: true,
message: 'Клики успешно обновлены',
externalAd: null,
}
} catch (error) {
console.error('Error updating external ad clicks:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка обновления кликов',
externalAd: null,
}
}
},
},
// Резолверы типов
Organization: {
users: async (parent: { id: string; users?: unknown[] }) => {
// Если пользователи уже загружены через include, возвращаем их
if (parent.users) {
return parent.users
}
// Иначе загружаем отдельно
return await prisma.user.findMany({
where: { organizationId: parent.id },
})
},
services: async (parent: { id: string; services?: unknown[] }) => {
// Если услуги уже загружены через include, возвращаем их
if (parent.services) {
return parent.services
}
// Иначе загружаем отдельно
return await prisma.service.findMany({
where: { organizationId: parent.id },
include: { organization: true },
orderBy: { createdAt: 'desc' },
})
},
supplies: async (parent: { id: string; supplies?: unknown[] }) => {
// Если расходники уже загружены через include, возвращаем их
if (parent.supplies) {
return parent.supplies
}
// Иначе загружаем отдельно
return await prisma.supply.findMany({
where: { organizationId: parent.id },
include: {
organization: true,
sellerOwner: true, // Включаем информацию о селлере-владельце
},
orderBy: { createdAt: 'desc' },
})
},
},
Cart: {
totalPrice: (parent: { items: Array<{ product: { price: number }; quantity: number }> }) => {
return parent.items.reduce((total, item) => {
return total + Number(item.product.price) * item.quantity
}, 0)
},
totalItems: (parent: { items: Array<{ quantity: number }> }) => {
return parent.items.reduce((total, item) => total + item.quantity, 0)
},
},
CartItem: {
totalPrice: (parent: { product: { price: number }; quantity: number }) => {
return Number(parent.product.price) * parent.quantity
},
isAvailable: (parent: { product: { quantity: number; isActive: boolean }; quantity: number }) => {
return parent.product.isActive && parent.product.quantity >= parent.quantity
},
availableQuantity: (parent: { product: { quantity: number } }) => {
return parent.product.quantity
},
},
User: {
organization: async (parent: { organizationId?: string; organization?: unknown }) => {
// Если организация уже загружена через include, возвращаем её
if (parent.organization) {
return parent.organization
}
// Иначе загружаем отдельно если есть organizationId
if (parent.organizationId) {
return await prisma.organization.findUnique({
where: { id: parent.organizationId },
include: {
apiKeys: true,
users: true,
},
})
}
return null
},
},
Product: {
type: (parent: { type?: string | null }) => parent.type || 'PRODUCT',
images: (parent: { images: unknown }) => {
// Если images это строка JSON, парсим её в массив
if (typeof parent.images === 'string') {
try {
return JSON.parse(parent.images)
} catch {
return []
}
}
// Если это уже массив, возвращаем как есть
if (Array.isArray(parent.images)) {
return parent.images
}
// Иначе возвращаем пустой массив
return []
},
},
Message: {
type: (parent: { type?: string | null }) => {
return parent.type || 'TEXT'
},
createdAt: (parent: { createdAt: Date | string }) => {
if (parent.createdAt instanceof Date) {
return parent.createdAt.toISOString()
}
return parent.createdAt
},
updatedAt: (parent: { updatedAt: Date | string }) => {
if (parent.updatedAt instanceof Date) {
return parent.updatedAt.toISOString()
}
return parent.updatedAt
},
},
Employee: {
fullName: (parent: { firstName: string; lastName: string; middleName?: string }) => {
const parts = [parent.lastName, parent.firstName]
if (parent.middleName) {
parts.push(parent.middleName)
}
return parts.join(' ')
},
name: (parent: { firstName: string; lastName: string }) => {
return `${parent.firstName} ${parent.lastName}`
},
birthDate: (parent: { birthDate?: Date | string | null }) => {
if (!parent.birthDate) return null
if (parent.birthDate instanceof Date) {
return parent.birthDate.toISOString()
}
return parent.birthDate
},
passportDate: (parent: { passportDate?: Date | string | null }) => {
if (!parent.passportDate) return null
if (parent.passportDate instanceof Date) {
return parent.passportDate.toISOString()
}
return parent.passportDate
},
hireDate: (parent: { hireDate: Date | string }) => {
if (parent.hireDate instanceof Date) {
return parent.hireDate.toISOString()
}
return parent.hireDate
},
createdAt: (parent: { createdAt: Date | string }) => {
if (parent.createdAt instanceof Date) {
return parent.createdAt.toISOString()
}
return parent.createdAt
},
updatedAt: (parent: { updatedAt: Date | string }) => {
if (parent.updatedAt instanceof Date) {
return parent.updatedAt.toISOString()
}
return parent.updatedAt
},
},
EmployeeSchedule: {
date: (parent: { date: Date | string }) => {
if (parent.date instanceof Date) {
return parent.date.toISOString()
}
return parent.date
},
createdAt: (parent: { createdAt: Date | string }) => {
if (parent.createdAt instanceof Date) {
return parent.createdAt.toISOString()
}
return parent.createdAt
},
updatedAt: (parent: { updatedAt: Date | string }) => {
if (parent.updatedAt instanceof Date) {
return parent.updatedAt.toISOString()
}
return parent.updatedAt
},
employee: async (parent: { employeeId: string }) => {
return await prisma.employee.findUnique({
where: { id: parent.employeeId },
})
},
},
}
// Мутации для категорий
const categoriesMutations = {
// Создать категорию
createCategory: async (_: unknown, args: { input: { name: string } }) => {
try {
// Проверяем есть ли уже категория с таким именем
const existingCategory = await prisma.category.findUnique({
where: { name: args.input.name },
})
if (existingCategory) {
return {
success: false,
message: 'Категория с таким названием уже существует',
}
}
const category = await prisma.category.create({
data: {
name: args.input.name,
},
})
return {
success: true,
message: 'Категория успешно создана',
category,
}
} catch (error) {
console.error('Ошибка создания категории:', error)
return {
success: false,
message: 'Ошибка при создании категории',
}
}
},
// Обновить категорию
updateCategory: async (_: unknown, args: { id: string; input: { name: string } }) => {
try {
// Проверяем существует ли категория
const existingCategory = await prisma.category.findUnique({
where: { id: args.id },
})
if (!existingCategory) {
return {
success: false,
message: 'Категория не найдена',
}
}
// Проверяем не занято ли имя другой категорией
const duplicateCategory = await prisma.category.findFirst({
where: {
name: args.input.name,
id: { not: args.id },
},
})
if (duplicateCategory) {
return {
success: false,
message: 'Категория с таким названием уже существует',
}
}
const category = await prisma.category.update({
where: { id: args.id },
data: {
name: args.input.name,
},
})
return {
success: true,
message: 'Категория успешно обновлена',
category,
}
} catch (error) {
console.error('Ошибка обновления категории:', error)
return {
success: false,
message: 'Ошибка при обновлении категории',
}
}
},
// Удалить категорию
deleteCategory: async (_: unknown, args: { id: string }) => {
try {
// Проверяем существует ли категория
const existingCategory = await prisma.category.findUnique({
where: { id: args.id },
})
if (!existingCategory) {
throw new GraphQLError('Категория не найдена')
}
// Проверяем есть ли товары в этой категории
const productsCount = await prisma.product.count({
where: { categoryId: args.id },
})
if (productsCount > 0) {
throw new GraphQLError('Нельзя удалить категорию, в которой есть товары')
}
await prisma.category.delete({
where: { id: args.id },
})
return true
} catch (error) {
console.error('Ошибка удаления категории:', error)
if (error instanceof GraphQLError) {
throw error
}
throw new GraphQLError('Ошибка при удалении категории')
}
},
}
// Логистические мутации
const logisticsMutations = {
// Создать логистический маршрут
createLogistics: async (
_: unknown,
args: {
input: {
fromLocation: string
toLocation: string
priceUnder1m3: number
priceOver1m3: number
description?: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
const logistics = await prisma.logistics.create({
data: {
fromLocation: args.input.fromLocation,
toLocation: args.input.toLocation,
priceUnder1m3: args.input.priceUnder1m3,
priceOver1m3: args.input.priceOver1m3,
description: args.input.description,
organizationId: currentUser.organization.id,
},
include: {
organization: true,
},
})
console.warn('✅ Logistics created:', logistics.id)
return {
success: true,
message: 'Логистический маршрут создан',
logistics,
}
} catch (error) {
console.error('❌ Error creating logistics:', error)
return {
success: false,
message: 'Ошибка при создании логистического маршрута',
}
}
},
// Обновить логистический маршрут
updateLogistics: async (
_: unknown,
args: {
id: string
input: {
fromLocation: string
toLocation: string
priceUnder1m3: number
priceOver1m3: number
description?: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
// Проверяем, что маршрут принадлежит организации пользователя
const existingLogistics = await prisma.logistics.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
})
if (!existingLogistics) {
throw new GraphQLError('Логистический маршрут не найден')
}
const logistics = await prisma.logistics.update({
where: { id: args.id },
data: {
fromLocation: args.input.fromLocation,
toLocation: args.input.toLocation,
priceUnder1m3: args.input.priceUnder1m3,
priceOver1m3: args.input.priceOver1m3,
description: args.input.description,
},
include: {
organization: true,
},
})
console.warn('✅ Logistics updated:', logistics.id)
return {
success: true,
message: 'Логистический маршрут обновлен',
logistics,
}
} catch (error) {
console.error('❌ Error updating logistics:', error)
return {
success: false,
message: 'Ошибка при обновлении логистического маршрута',
}
}
},
// Удалить логистический маршрут
deleteLogistics: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
// Проверяем, что маршрут принадлежит организации пользователя
const existingLogistics = await prisma.logistics.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
})
if (!existingLogistics) {
throw new GraphQLError('Логистический маршрут не найден')
}
await prisma.logistics.delete({
where: { id: args.id },
})
console.warn('✅ Logistics deleted:', args.id)
return true
} catch (error) {
console.error('❌ Error deleting logistics:', error)
return false
}
},
}
// Добавляем дополнительные мутации к основным резолверам
resolvers.Mutation = {
...resolvers.Mutation,
...categoriesMutations,
...logisticsMutations,
}
// Админ резолверы
const adminQueries = {
adminMe: async (_: unknown, __: unknown, context: Context) => {
if (!context.admin) {
throw new GraphQLError('Требуется авторизация администратора', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const admin = await prisma.admin.findUnique({
where: { id: context.admin.id },
})
if (!admin) {
throw new GraphQLError('Администратор не найден')
}
return admin
},
allUsers: async (_: unknown, args: { search?: string; limit?: number; offset?: number }, context: Context) => {
if (!context.admin) {
throw new GraphQLError('Требуется авторизация администратора', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const limit = args.limit || 50
const offset = args.offset || 0
// Строим условие поиска
const whereCondition: Prisma.UserWhereInput = args.search
? {
OR: [
{ phone: { contains: args.search, mode: 'insensitive' } },
{ managerName: { contains: args.search, mode: 'insensitive' } },
{
organization: {
OR: [
{ name: { contains: args.search, mode: 'insensitive' } },
{ fullName: { contains: args.search, mode: 'insensitive' } },
{ inn: { contains: args.search, mode: 'insensitive' } },
],
},
},
],
}
: {}
// Получаем пользователей с пагинацией
const [users, total] = await Promise.all([
prisma.user.findMany({
where: whereCondition,
include: {
organization: true,
},
take: limit,
skip: offset,
orderBy: { createdAt: 'desc' },
}),
prisma.user.count({
where: whereCondition,
}),
])
return {
users,
total,
hasMore: offset + limit < total,
}
},
}
const adminMutations = {
adminLogin: async (_: unknown, args: { username: string; password: string }) => {
try {
// Найти администратора
const admin = await prisma.admin.findUnique({
where: { username: args.username },
})
if (!admin) {
return {
success: false,
message: 'Неверные учетные данные',
}
}
// Проверить активность
if (!admin.isActive) {
return {
success: false,
message: 'Аккаунт заблокирован',
}
}
// Проверить пароль
const isPasswordValid = await bcrypt.compare(args.password, admin.password)
if (!isPasswordValid) {
return {
success: false,
message: 'Неверные учетные данные',
}
}
// Обновить время последнего входа
await prisma.admin.update({
where: { id: admin.id },
data: { lastLogin: new Date() },
})
// Создать токен
const token = jwt.sign(
{
adminId: admin.id,
username: admin.username,
type: 'admin',
},
process.env.JWT_SECRET!,
{ expiresIn: '24h' },
)
return {
success: true,
message: 'Успешная авторизация',
token,
admin: {
...admin,
password: undefined, // Не возвращаем пароль
},
}
} catch (error) {
console.error('Admin login error:', error)
return {
success: false,
message: 'Ошибка авторизации',
}
}
},
adminLogout: async (_: unknown, __: unknown, context: Context) => {
if (!context.admin) {
throw new GraphQLError('Требуется авторизация администратора', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
return true
},
}
// Wildberries статистика
const wildberriesQueries = {
debugWildberriesAdverts: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
if (!user?.organization || user.organization.type !== 'SELLER') {
throw new GraphQLError('Доступно только для продавцов')
}
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
if (!wbApiKeyRecord) {
throw new GraphQLError('WB API ключ не настроен')
}
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
// Получаем кампании во всех статусах
const [active, completed, paused] = await Promise.all([
wbService.getAdverts(9).catch(() => []), // активные
wbService.getAdverts(7).catch(() => []), // завершенные
wbService.getAdverts(11).catch(() => []), // на паузе
])
const allCampaigns = [...active, ...completed, ...paused]
return {
success: true,
message: `Found ${active.length} active, ${completed.length} completed, ${paused.length} paused campaigns`,
campaignsCount: allCampaigns.length,
campaigns: allCampaigns.map((c) => ({
id: c.advertId,
name: c.name,
status: c.status,
type: c.type,
})),
}
} catch (error) {
console.error('Error debugging WB adverts:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error',
campaignsCount: 0,
campaigns: [],
}
}
},
getWildberriesStatistics: async (
_: unknown,
{
period,
startDate,
endDate,
}: {
period?: 'week' | 'month' | 'quarter'
startDate?: string
endDate?: string
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
// Получаем организацию пользователя и её WB API ключ
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
if (user.organization.type !== 'SELLER') {
throw new GraphQLError('Доступно только для продавцов')
}
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
if (!wbApiKeyRecord) {
throw new GraphQLError('WB API ключ не настроен')
}
// Создаем экземпляр сервиса
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
// Получаем даты
let dateFrom: string
let dateTo: string
if (startDate && endDate) {
// Используем пользовательские даты
dateFrom = startDate
dateTo = endDate
} else if (period) {
// Используем предустановленный период
dateFrom = WildberriesService.getDatePeriodAgo(period)
dateTo = WildberriesService.formatDate(new Date())
} else {
throw new GraphQLError('Необходимо указать либо period, либо startDate и endDate')
}
// Получаем статистику
const statistics = await wbService.getStatistics(dateFrom, dateTo)
return {
success: true,
data: statistics,
message: null,
}
} catch (error) {
console.error('Error fetching WB statistics:', error)
// Фолбэк: пробуем вернуть последние данные из кеша статистики селлера
try {
const user = await prisma.user.findUnique({
where: { id: context.user!.id },
include: { organization: true },
})
if (user?.organization) {
const whereCache: any = {
organizationId: user.organization.id,
period: startDate && endDate ? 'custom' : (period ?? 'week'),
}
if (startDate && endDate) {
whereCache.dateFrom = new Date(startDate)
whereCache.dateTo = new Date(endDate)
}
const cache = await prisma.sellerStatsCache.findFirst({
where: whereCache,
orderBy: { createdAt: 'desc' },
})
if (cache?.productsData) {
// Ожидаем, что productsData — строка JSON с полями, сохраненными клиентом
const parsed = JSON.parse(cache.productsData as unknown as string) as {
tableData?: Array<{
date: string
salesUnits: number
orders: number
advertising: number
refusals: number
returns: number
revenue: number
buyoutPercentage: number
}>
}
const table = parsed.tableData ?? []
const dataFromCache = table.map((row) => ({
date: row.date,
sales: row.salesUnits,
orders: row.orders,
advertising: row.advertising,
refusals: row.refusals,
returns: row.returns,
revenue: row.revenue,
buyoutPercentage: row.buyoutPercentage,
}))
if (dataFromCache.length > 0) {
return {
success: true,
data: dataFromCache,
message: 'Данные возвращены из кеша из-за ошибки WB API',
}
}
} else if (cache?.advertisingData) {
// Fallback №2: если нет productsData, но есть advertisingData —
// формируем минимальный набор данных по дням на основе затрат на рекламу
try {
const adv = JSON.parse(cache.advertisingData as unknown as string) as {
dailyData?: Array<{
date: string
totalSum?: number
totalOrders?: number
totalRevenue?: number
}>
}
const daily = adv.dailyData ?? []
const dataFromAdv = daily.map((d) => ({
date: d.date,
sales: 0,
orders: typeof d.totalOrders === 'number' ? d.totalOrders : 0,
advertising: typeof d.totalSum === 'number' ? d.totalSum : 0,
refusals: 0,
returns: 0,
revenue: typeof d.totalRevenue === 'number' ? d.totalRevenue : 0,
buyoutPercentage: 0,
}))
if (dataFromAdv.length > 0) {
return {
success: true,
data: dataFromAdv,
message: 'Данные по продажам недоступны из-за ошибки WB API. Показаны данные по рекламе из кеша.',
}
}
} catch (parseErr) {
console.error('Failed to parse advertisingData from cache:', parseErr)
}
}
}
} catch (fallbackErr) {
console.error('Seller stats cache fallback failed:', fallbackErr)
}
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка получения статистики',
data: [],
}
}
},
getWildberriesCampaignStats: async (
_: unknown,
{
input,
}: {
input: {
campaigns: Array<{
id: number
dates?: string[]
interval?: {
begin: string
end: string
}
}>
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
// Получаем организацию пользователя и её WB API ключ
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
if (user.organization.type !== 'SELLER') {
throw new GraphQLError('Доступно только для продавцов')
}
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
if (!wbApiKeyRecord) {
throw new GraphQLError('WB API ключ не настроен')
}
// Создаем экземпляр сервиса
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
// Преобразуем запросы в нужный формат
const requests = input.campaigns.map((campaign) => {
if (campaign.dates && campaign.dates.length > 0) {
return {
id: campaign.id,
dates: campaign.dates,
}
} else if (campaign.interval) {
return {
id: campaign.id,
interval: campaign.interval,
}
} else {
// Если не указаны ни даты, ни интервал, возвращаем данные только за последние сутки
return {
id: campaign.id,
}
}
})
// Получаем статистику кампаний
const campaignStats = await wbService.getCampaignStats(requests)
return {
success: true,
data: campaignStats,
message: null,
}
} catch (error) {
console.error('Error fetching WB campaign stats:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка получения статистики кампаний',
data: [],
}
}
},
getWildberriesCampaignsList: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
// Получаем организацию пользователя и её WB API ключ
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: {
include: {
apiKeys: true,
},
},
},
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
if (user.organization.type !== 'SELLER') {
throw new GraphQLError('Доступно только для продавцов')
}
const wbApiKeyRecord = user.organization.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES' && key.isActive)
if (!wbApiKeyRecord) {
throw new GraphQLError('WB API ключ не настроен')
}
// Создаем экземпляр сервиса
const wbService = new WildberriesService(wbApiKeyRecord.apiKey)
// Получаем список кампаний
const campaignsList = await wbService.getCampaignsList()
return {
success: true,
data: campaignsList,
message: null,
}
} catch (error) {
console.error('Error fetching WB campaigns list:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка получения списка кампаний',
data: {
adverts: [],
all: 0,
},
}
}
},
// Получение заявок покупателей на возврат от Wildberries от всех партнеров-селлеров
wbReturnClaims: async (
_: unknown,
{ isArchive, limit, offset }: { isArchive: boolean; limit?: number; offset?: number },
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
// Получаем текущую организацию пользователя (фулфилмент)
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: {
organization: true,
},
})
if (!user?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что это фулфилмент организация
if (user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступ только для фулфилмент организаций')
}
// Получаем всех партнеров-селлеров с активными WB API ключами
const partnerSellerOrgs = await prisma.counterparty.findMany({
where: {
organizationId: user.organization.id,
},
include: {
counterparty: {
include: {
apiKeys: {
where: {
marketplace: 'WILDBERRIES',
isActive: true,
},
},
},
},
},
})
// Фильтруем только селлеров с WB API ключами
const sellersWithWbKeys = partnerSellerOrgs.filter(
(partner) => partner.counterparty.type === 'SELLER' && partner.counterparty.apiKeys.length > 0,
)
if (sellersWithWbKeys.length === 0) {
return {
claims: [],
total: 0,
}
}
console.warn(`Found ${sellersWithWbKeys.length} seller partners with WB keys`)
// Получаем заявки от всех селлеров параллельно
const claimsPromises = sellersWithWbKeys.map(async (partner) => {
const wbApiKey = partner.counterparty.apiKeys[0].apiKey
const wbService = new WildberriesService(wbApiKey)
try {
const claimsResponse = await wbService.getClaims({
isArchive,
limit: Math.ceil((limit || 50) / sellersWithWbKeys.length), // Распределяем лимит между селлерами
offset: 0,
})
// Добавляем информацию о селлере к каждой заявке
const claimsWithSeller = claimsResponse.claims.map((claim) => ({
...claim,
sellerOrganization: {
id: partner.counterparty.id,
name: partner.counterparty.name || 'Неизвестная организация',
inn: partner.counterparty.inn || '',
},
}))
console.warn(`Got ${claimsWithSeller.length} claims from seller ${partner.counterparty.name}`)
return claimsWithSeller
} catch (error) {
console.error(`Error fetching claims for seller ${partner.counterparty.name}:`, error)
return []
}
})
const allClaims = (await Promise.all(claimsPromises)).flat()
console.warn(`Total claims aggregated: ${allClaims.length}`)
// Сортируем по дате создания (новые первыми)
allClaims.sort((a, b) => new Date(b.dt).getTime() - new Date(a.dt).getTime())
// Применяем пагинацию
const paginatedClaims = allClaims.slice(offset || 0, (offset || 0) + (limit || 50))
console.warn(`Paginated claims: ${paginatedClaims.length}`)
// Преобразуем в формат фронтенда
const transformedClaims = paginatedClaims.map((claim) => ({
id: claim.id,
claimType: claim.claim_type,
status: claim.status,
statusEx: claim.status_ex,
nmId: claim.nm_id,
userComment: claim.user_comment || '',
wbComment: claim.wb_comment || null,
dt: claim.dt,
imtName: claim.imt_name,
orderDt: claim.order_dt,
dtUpdate: claim.dt_update,
photos: claim.photos || [],
videoPaths: claim.video_paths || [],
actions: claim.actions || [],
price: claim.price,
currencyCode: claim.currency_code,
srid: claim.srid,
sellerOrganization: claim.sellerOrganization,
}))
console.warn(`Returning ${transformedClaims.length} transformed claims to frontend`)
return {
claims: transformedClaims,
total: allClaims.length,
}
} catch (error) {
console.error('Error fetching WB return claims:', error)
throw new GraphQLError(error instanceof Error ? error.message : 'Ошибка получения заявок на возврат')
}
},
}
// Резолверы для внешней рекламы
const externalAdQueries = {
getExternalAds: async (_: unknown, { dateFrom, dateTo }: { dateFrom: string; dateTo: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
const externalAds = await prisma.externalAd.findMany({
where: {
organizationId: user.organization.id,
date: {
gte: new Date(dateFrom),
lte: new Date(dateTo + 'T23:59:59.999Z'),
},
},
orderBy: {
date: 'desc',
},
})
return {
success: true,
message: null,
externalAds: externalAds.map((ad) => ({
...ad,
cost: parseFloat(ad.cost.toString()),
date: ad.date.toISOString().split('T')[0],
createdAt: ad.createdAt.toISOString(),
updatedAt: ad.updatedAt.toISOString(),
})),
}
} catch (error) {
console.error('Error fetching external ads:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка получения внешней рекламы',
externalAds: [],
}
}
},
}
const externalAdMutations = {
createExternalAd: async (
_: unknown,
{
input,
}: {
input: {
name: string
url: string
cost: number
date: string
nmId: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
const externalAd = await prisma.externalAd.create({
data: {
name: input.name,
url: input.url,
cost: input.cost,
date: new Date(input.date),
nmId: input.nmId,
organizationId: user.organization.id,
},
})
return {
success: true,
message: 'Внешняя реклама успешно создана',
externalAd: {
...externalAd,
cost: parseFloat(externalAd.cost.toString()),
date: externalAd.date.toISOString().split('T')[0],
createdAt: externalAd.createdAt.toISOString(),
updatedAt: externalAd.updatedAt.toISOString(),
},
}
} catch (error) {
console.error('Error creating external ad:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка создания внешней рекламы',
externalAd: null,
}
}
},
updateExternalAd: async (
_: unknown,
{
id,
input,
}: {
id: string
input: {
name: string
url: string
cost: number
date: string
nmId: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
// Проверяем, что реклама принадлежит организации пользователя
const existingAd = await prisma.externalAd.findFirst({
where: {
id,
organizationId: user.organization.id,
},
})
if (!existingAd) {
throw new GraphQLError('Внешняя реклама не найдена')
}
const externalAd = await prisma.externalAd.update({
where: { id },
data: {
name: input.name,
url: input.url,
cost: input.cost,
date: new Date(input.date),
nmId: input.nmId,
},
})
return {
success: true,
message: 'Внешняя реклама успешно обновлена',
externalAd: {
...externalAd,
cost: parseFloat(externalAd.cost.toString()),
date: externalAd.date.toISOString().split('T')[0],
createdAt: externalAd.createdAt.toISOString(),
updatedAt: externalAd.updatedAt.toISOString(),
},
}
} catch (error) {
console.error('Error updating external ad:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка обновления внешней рекламы',
externalAd: null,
}
}
},
deleteExternalAd: async (_: unknown, { id }: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
// Проверяем, что реклама принадлежит организации пользователя
const existingAd = await prisma.externalAd.findFirst({
where: {
id,
organizationId: user.organization.id,
},
})
if (!existingAd) {
throw new GraphQLError('Внешняя реклама не найдена')
}
await prisma.externalAd.delete({
where: { id },
})
return {
success: true,
message: 'Внешняя реклама успешно удалена',
externalAd: null,
}
} catch (error) {
console.error('Error deleting external ad:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка удаления внешней рекламы',
externalAd: null,
}
}
},
}
// Резолверы для кеша склада WB
const wbWarehouseCacheQueries = {
getWBWarehouseData: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
// Получаем текущую дату без времени
const today = new Date()
today.setHours(0, 0, 0, 0)
// Ищем кеш за сегодня
const cache = await prisma.wBWarehouseCache.findFirst({
where: {
organizationId: user.organization.id,
cacheDate: today,
},
orderBy: {
createdAt: 'desc',
},
})
if (cache) {
// Возвращаем данные из кеша
return {
success: true,
message: 'Данные получены из кеша',
cache: {
...cache,
cacheDate: cache.cacheDate.toISOString().split('T')[0],
createdAt: cache.createdAt.toISOString(),
updatedAt: cache.updatedAt.toISOString(),
},
fromCache: true,
}
} else {
// Кеша нет, нужно загрузить данные из API
return {
success: true,
message: 'Кеш не найден, требуется загрузка из API',
cache: null,
fromCache: false,
}
}
} catch (error) {
console.error('Error getting WB warehouse cache:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка получения кеша склада WB',
cache: null,
fromCache: false,
}
}
},
}
const wbWarehouseCacheMutations = {
saveWBWarehouseCache: async (
_: unknown,
{
input,
}: {
input: {
data: string
totalProducts: number
totalStocks: number
totalReserved: number
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
// Получаем текущую дату без времени
const today = new Date()
today.setHours(0, 0, 0, 0)
// Используем upsert для создания или обновления кеша
const cache = await prisma.wBWarehouseCache.upsert({
where: {
organizationId_cacheDate: {
organizationId: user.organization.id,
cacheDate: today,
},
},
update: {
data: input.data,
totalProducts: input.totalProducts,
totalStocks: input.totalStocks,
totalReserved: input.totalReserved,
},
create: {
organizationId: user.organization.id,
cacheDate: today,
data: input.data,
totalProducts: input.totalProducts,
totalStocks: input.totalStocks,
totalReserved: input.totalReserved,
},
})
return {
success: true,
message: 'Кеш склада WB успешно сохранен',
cache: {
...cache,
cacheDate: cache.cacheDate.toISOString().split('T')[0],
createdAt: cache.createdAt.toISOString(),
updatedAt: cache.updatedAt.toISOString(),
},
fromCache: false,
}
} catch (error) {
console.error('Error saving WB warehouse cache:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка сохранения кеша склада WB',
cache: null,
fromCache: false,
}
}
},
}
// Добавляем админ запросы и мутации к основным резолверам
resolvers.Query = {
...resolvers.Query,
...adminQueries,
...wildberriesQueries,
...externalAdQueries,
...wbWarehouseCacheQueries,
// Кеш статистики селлера
getSellerStatsCache: async (
_: unknown,
args: { period: string; dateFrom?: string | null; dateTo?: string | null },
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)
// Для custom учитываем диапазон, иначе только period
const where: any = {
organizationId: user.organization.id,
cacheDate: today,
period: args.period,
}
if (args.period === 'custom') {
if (!args.dateFrom || !args.dateTo) {
throw new GraphQLError('Для custom необходимо указать dateFrom и dateTo')
}
where.dateFrom = new Date(args.dateFrom)
where.dateTo = new Date(args.dateTo)
}
const cache = await prisma.sellerStatsCache.findFirst({
where,
orderBy: { createdAt: 'desc' },
})
if (!cache) {
return {
success: true,
message: 'Кеш не найден',
cache: null,
fromCache: false,
}
}
// Если кеш просрочен — не используем его, как и для склада WB (сервер решает, годен ли кеш)
const now = new Date()
if (cache.expiresAt && cache.expiresAt <= now) {
return {
success: true,
message: 'Кеш устарел, требуется загрузка из API',
cache: null,
fromCache: false,
}
}
return {
success: true,
message: 'Данные получены из кеша',
cache: {
...cache,
cacheDate: cache.cacheDate.toISOString().split('T')[0],
dateFrom: cache.dateFrom ? cache.dateFrom.toISOString().split('T')[0] : null,
dateTo: cache.dateTo ? cache.dateTo.toISOString().split('T')[0] : null,
productsTotalSales: cache.productsTotalSales ? Number(cache.productsTotalSales) : null,
advertisingTotalCost: cache.advertisingTotalCost ? Number(cache.advertisingTotalCost) : null,
// Возвращаем expiresAt в ISO, чтобы клиент корректно парсил дату
expiresAt: cache.expiresAt.toISOString(),
createdAt: cache.createdAt.toISOString(),
updatedAt: cache.updatedAt.toISOString(),
},
fromCache: true,
}
} catch (error) {
console.error('Error getting Seller Stats cache:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка получения кеша статистики',
cache: null,
fromCache: false,
}
}
},
}
resolvers.Mutation = {
...resolvers.Mutation,
...adminMutations,
...externalAdMutations,
...wbWarehouseCacheMutations,
// Сохранение кеша статистики селлера
saveSellerStatsCache: async (
_: unknown,
{
input,
}: {
input: {
period: string
dateFrom?: string | null
dateTo?: string | null
productsData?: string | null
productsTotalSales?: number | null
productsTotalOrders?: number | null
productsCount?: number | null
advertisingData?: string | null
advertisingTotalCost?: number | null
advertisingTotalViews?: number | null
advertisingTotalClicks?: number | null
expiresAt: 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 today = new Date()
today.setHours(0, 0, 0, 0)
const data: any = {
organizationId: user.organization.id,
cacheDate: today,
period: input.period,
dateFrom: input.period === 'custom' && input.dateFrom ? new Date(input.dateFrom) : null,
dateTo: input.period === 'custom' && input.dateTo ? new Date(input.dateTo) : null,
productsData: input.productsData ?? null,
productsTotalSales: input.productsTotalSales ?? null,
productsTotalOrders: input.productsTotalOrders ?? null,
productsCount: input.productsCount ?? null,
advertisingData: input.advertisingData ?? null,
advertisingTotalCost: input.advertisingTotalCost ?? null,
advertisingTotalViews: input.advertisingTotalViews ?? null,
advertisingTotalClicks: input.advertisingTotalClicks ?? null,
expiresAt: new Date(input.expiresAt),
}
// upsert с составным уникальным ключом, содержащим NULL, в Prisma вызывает валидацию.
// Делаем вручную: findFirst по уникальному набору, затем update или create.
const existing = await prisma.sellerStatsCache.findFirst({
where: {
organizationId: user.organization.id,
cacheDate: today,
period: input.period,
dateFrom: data.dateFrom,
dateTo: data.dateTo,
},
})
const cache = existing
? await prisma.sellerStatsCache.update({ where: { id: existing.id }, data })
: await prisma.sellerStatsCache.create({ data })
return {
success: true,
message: 'Кеш статистики сохранен',
cache: {
...cache,
cacheDate: cache.cacheDate.toISOString().split('T')[0],
dateFrom: cache.dateFrom ? cache.dateFrom.toISOString().split('T')[0] : null,
dateTo: cache.dateTo ? cache.dateTo.toISOString().split('T')[0] : null,
productsTotalSales: cache.productsTotalSales ? Number(cache.productsTotalSales) : null,
advertisingTotalCost: cache.advertisingTotalCost ? Number(cache.advertisingTotalCost) : null,
createdAt: cache.createdAt.toISOString(),
updatedAt: cache.updatedAt.toISOString(),
},
fromCache: false,
}
} catch (error) {
console.error('Error saving Seller Stats cache:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка сохранения кеша статистики',
cache: null,
fromCache: false,
}
}
},
// ВОССТАНОВЛЕННЫЕ v2 mutations из бэкапов
...fulfillmentConsumableV2MutationsRestored,
// V2 mutations для логистики
...logisticsConsumableV2Mutations,
// V2 mutations для поставок расходников селлера
...sellerConsumableMutations,
// V2 mutations для услуг фулфилмента
...fulfillmentServicesMutations,
// V2 mutations для товарных поставок селлера
...sellerGoodsMutations,
}
/* // Резолвер для парсинга JSON рецептуры в SupplyOrderItem
SupplyOrderItem: {
recipe: (parent: any) => {
// Если recipe это JSON строка, парсим её
if (typeof parent.recipe === 'string') {
try {
return JSON.parse(parent.recipe)
} catch (error) {
console.error('Error parsing recipe JSON:', error)
return null
}
}
// Если recipe уже объект, возвращаем как есть
return parent.recipe
},
},
*/
// ===============================================
// НОВАЯ СИСТЕМА ПОСТАВОК V2.0 - RESOLVERS
// ===============================================
export default resolvers

View File

@ -3,6 +3,7 @@ import { GraphQLError } from 'graphql'
import { Context } from '../../context'
import { prisma } from '../../../lib/prisma'
import { notifyOrganization } from '../../../lib/realtime'
import { processSupplyOrderReceipt } from '../../../lib/inventory-management'
import { DomainResolvers } from '../shared/types'
import {
getCurrentUser,
@ -948,6 +949,137 @@ export const inventoryResolvers: DomainResolvers = {
}
}),
// Фулфилмент принимает поставку расходников
fulfillmentReceiveConsumableSupply: withAuth(async (
_: unknown,
args: {
id: string
items: Array<{ id: string; receivedQuantity: number; defectQuantity?: number }>
notes?: string
},
context: Context,
) => {
console.log('🔍 FULFILLMENT_RECEIVE_CONSUMABLE_SUPPLY DOMAIN MUTATION STARTED:', {
userId: context.user?.id,
supplyId: args.id,
itemsCount: args.items.length
})
try {
const user = await checkFulfillmentAccess(context.user!.id)
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.fulfillmentCenterId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (supply.status !== 'SHIPPED') {
throw new GraphQLError('Поставку можно принять только в статусе SHIPPED')
}
// Обновляем статус поставки на DELIVERED
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'DELIVERED',
receivedAt: new Date(),
receivedById: user.id,
receiptNotes: args.notes,
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
// Обновляем фактические количества товаров
for (const itemData of args.items) {
await prisma.fulfillmentConsumableSupplyItem.updateMany({
where: { id: itemData.id },
data: {
receivedQuantity: itemData.receivedQuantity,
defectQuantity: itemData.defectQuantity || 0,
},
})
}
// Обновляем складские остатки в FulfillmentConsumableInventory
const inventoryItems = args.items.map(item => {
const supplyItem = supply.items.find(si => si.id === item.id)
if (!supplyItem) {
throw new GraphQLError(`Товар поставки не найден: ${item.id}`)
}
return {
productId: supplyItem.productId,
receivedQuantity: item.receivedQuantity,
unitPrice: parseFloat(supplyItem.unitPrice.toString()),
}
})
await processSupplyOrderReceipt(supply.id, inventoryItems)
console.log('✅ FULFILLMENT_RECEIVE_SUPPLY: Inventory updated:', {
supplyId: supply.id,
itemsCount: inventoryItems.length,
totalReceived: inventoryItems.reduce((sum, item) => sum + item.receivedQuantity, 0),
})
// Уведомляем поставщика о приемке
if (supply.supplierId) {
await notifyOrganization(supply.supplierId, {
type: 'supply-order:delivered',
title: 'Поставка принята фулфилментом',
message: `Фулфилмент-центр "${supply.fulfillmentCenter.name}" принял поставку`,
data: {
supplyOrderId: supply.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
fulfillmentCenterName: supply.fulfillmentCenter.name,
itemsCount: inventoryItems.length,
totalReceived: inventoryItems.reduce((sum, item) => sum + item.receivedQuantity, 0),
},
})
}
const result = {
success: true,
message: 'Поставка успешно принята',
order: updatedSupply,
}
console.log('✅ FULFILLMENT_RECEIVE_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyId: updatedSupply.id })
return result
} catch (error: any) {
console.error('❌ FULFILLMENT_RECEIVE_CONSUMABLE_SUPPLY DOMAIN ERROR:', error)
return {
success: false,
message: error.message || 'Ошибка приемки поставки',
order: null,
}
}
}),
// V1 Legacy: Резервирование товара на складе
reserveProductStock: async (
_: unknown,

View File

@ -1,406 +0,0 @@
// =============================================================================
// 🧑‍💼 EMPLOYEE V2 RESOLVERS
// =============================================================================
// Полные резолверы для системы управления сотрудниками V2
import type { Prisma } from '@prisma/client'
import { GraphQLError } from 'graphql'
import { prisma } from '../../lib/prisma'
import type { Context } from '../context'
// =============================================================================
// 🔐 HELPERS
// =============================================================================
const withAuth = (resolver: any) => {
return async (parent: any, args: any, context: Context) => {
console.log('🔐 WITHAUTH CHECK:', {
hasUser: !!context.user,
userId: context.user?.id,
organizationId: context.user?.organizationId,
})
if (!context.user) {
console.error('❌ AUTH FAILED: No user in context')
throw new GraphQLError('Не авторизован', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
console.log('✅ AUTH PASSED: Calling resolver')
try {
const result = await resolver(parent, args, context)
console.log('🎯 RESOLVER RESULT TYPE:', typeof result, result === null ? 'NULL RESULT!' : 'Has result')
return result
} catch (error) {
console.error('💥 RESOLVER ERROR:', error)
throw error
}
}
}
const checkOrganizationAccess = async (userId: string) => {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { organization: true },
})
if (!user?.organizationId) {
throw new GraphQLError('Пользователь не привязан к организации', {
extensions: { code: 'FORBIDDEN' },
})
}
return user
}
// =============================================================================
// 🔄 TRANSFORM HELPERS
// =============================================================================
function transformEmployeeToV2(employee: any): any {
return {
id: employee.id,
personalInfo: {
firstName: employee.firstName,
lastName: employee.lastName,
middleName: employee.middleName,
fullName: `${employee.lastName} ${employee.firstName} ${employee.middleName || ''}`.trim(),
birthDate: employee.birthDate,
avatar: employee.avatar,
},
documentsInfo: {
passportPhoto: employee.passportPhoto,
passportSeries: employee.passportSeries,
passportNumber: employee.passportNumber,
passportIssued: employee.passportIssued,
passportDate: employee.passportDate,
},
contactInfo: {
phone: employee.phone,
email: employee.email,
telegram: employee.telegram,
whatsapp: employee.whatsapp,
address: employee.address,
emergencyContact: employee.emergencyContact,
emergencyPhone: employee.emergencyPhone,
},
workInfo: {
position: employee.position,
department: employee.department,
hireDate: employee.hireDate,
salary: employee.salary,
status: employee.status,
},
organizationId: employee.organizationId,
organization: employee.organization ? {
id: employee.organization.id,
name: employee.organization.name,
fullName: employee.organization.fullName,
type: employee.organization.type,
} : undefined,
scheduleRecords: employee.scheduleRecords?.map(transformScheduleToV2),
metadata: {
createdAt: employee.createdAt,
updatedAt: employee.updatedAt,
},
}
}
function transformScheduleToV2(record: any): any {
return {
id: record.id,
employeeId: record.employeeId,
date: record.date,
status: record.status,
hoursWorked: record.hoursWorked,
overtimeHours: record.overtimeHours,
notes: record.notes,
metadata: {
createdAt: record.createdAt,
updatedAt: record.updatedAt,
},
}
}
// =============================================================================
// 🔍 QUERY RESOLVERS V2
// =============================================================================
export const employeeQueriesV2 = {
// Получение сотрудников с фильтрацией и пагинацией
employeesV2: withAuth(async (_: unknown, args: any, context: Context) => {
console.log('🔍 EMPLOYEE V2 QUERY STARTED:', { args, userId: context.user?.id })
try {
const { input = {} } = args
const {
status,
department,
search,
page = 1,
limit = 20,
sortBy = 'CREATED_AT',
sortOrder = 'DESC',
} = input
const user = await checkOrganizationAccess(context.user!.id)
// Построение условий фильтрации
const where: Prisma.EmployeeWhereInput = {
organizationId: user.organizationId!,
...(status?.length && { status: { in: status } }),
...(department && { department }),
...(search && {
OR: [
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
{ position: { contains: search, mode: 'insensitive' } },
{ phone: { contains: search } },
],
}),
}
// Подсчет общего количества
const total = await prisma.employee.count({ where })
// Получение данных с пагинацией
const employees = await prisma.employee.findMany({
where,
include: {
organization: true,
scheduleRecords: {
orderBy: { date: 'desc' },
take: 10,
},
},
skip: (page - 1) * limit,
take: limit,
orderBy: {
[sortBy === 'NAME' ? 'firstName' :
sortBy === 'HIRE_DATE' ? 'hireDate' :
sortBy === 'STATUS' ? 'status' : 'createdAt']:
sortOrder.toLowerCase() as 'asc' | 'desc',
},
})
// Подсчет статистики
const stats = {
total,
active: await prisma.employee.count({
where: { ...where, status: 'ACTIVE' },
}),
vacation: await prisma.employee.count({
where: { ...where, status: 'VACATION' },
}),
sick: await prisma.employee.count({
where: { ...where, status: 'SICK' },
}),
fired: await prisma.employee.count({
where: { ...where, status: 'FIRED' },
}),
averageSalary: 0, // Пока не реализовано
}
const result = {
items: employees.map(transformEmployeeToV2),
pagination: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
stats,
}
console.log('✅ EMPLOYEE V2 QUERY SUCCESS:', { itemsCount: result.items.length, total })
return result
} catch (error: any) {
console.error('❌ EMPLOYEES V2 QUERY ERROR:', error)
throw error // Пробрасываем ошибку вместо возврата null
}
}),
employeeV2: withAuth(async (_: unknown, args: { id: string }, context: Context) => {
const user = await checkOrganizationAccess(context.user!.id)
const employee = await prisma.employee.findFirst({
where: {
id: args.id,
organizationId: user.organizationId!,
},
include: {
organization: true,
scheduleRecords: true,
},
})
if (!employee) {
throw new GraphQLError('Сотрудник не найден')
}
return transformEmployeeToV2(employee)
}),
}
// =============================================================================
// 🔧 MUTATION RESOLVERS V2
// =============================================================================
export const employeeMutationsV2 = {
createEmployeeV2: withAuth(async (_: unknown, args: any, context: Context) => {
console.log('🔍 CREATE EMPLOYEE V2 MUTATION STARTED:', { args, userId: context.user?.id })
const { input } = args
const user = await checkOrganizationAccess(context.user!.id)
try {
const employee = await prisma.employee.create({
data: {
organizationId: user.organizationId!,
firstName: input.personalInfo.firstName,
lastName: input.personalInfo.lastName,
middleName: input.personalInfo.middleName,
birthDate: input.personalInfo.birthDate,
avatar: input.personalInfo.avatar,
...input.documentsInfo,
...input.contactInfo,
...input.workInfo,
},
include: {
organization: true,
},
})
const result = {
success: true,
message: 'Сотрудник успешно создан',
employee: transformEmployeeToV2(employee),
errors: [],
}
console.log('✅ CREATE EMPLOYEE V2 SUCCESS:', { employeeId: employee.id, result })
return result
} catch (error: any) {
console.error('❌ CREATE EMPLOYEE V2 ERROR:', error)
throw error // Пробрасываем ошибку для правильной диагностики
}
}),
updateEmployeeV2: withAuth(async (_: unknown, args: any, context: Context) => {
const { id, input } = args
const user = await checkOrganizationAccess(context.user!.id)
try {
const existing = await prisma.employee.findFirst({
where: { id, organizationId: user.organizationId! },
})
if (!existing) {
return {
success: false,
message: 'Сотрудник не найден',
employee: null,
errors: [{ field: 'id', message: 'Сотрудник не найден' }],
}
}
const updateData: any = {}
if (input.personalInfo) Object.assign(updateData, input.personalInfo)
if (input.documentsInfo) Object.assign(updateData, input.documentsInfo)
if (input.contactInfo) Object.assign(updateData, input.contactInfo)
if (input.workInfo) Object.assign(updateData, input.workInfo)
const employee = await prisma.employee.update({
where: { id },
data: updateData,
include: { organization: true },
})
return {
success: true,
message: 'Сотрудник успешно обновлен',
employee: transformEmployeeToV2(employee),
errors: [],
}
} catch (error: any) {
console.error('Error updating employee:', error)
return {
success: false,
message: 'Ошибка при обновлении сотрудника',
employee: null,
errors: [{ field: 'general', message: error.message }],
}
}
}),
deleteEmployeeV2: withAuth(async (_: unknown, args: { id: string }, context: Context) => {
const user = await checkOrganizationAccess(context.user!.id)
const existing = await prisma.employee.findFirst({
where: { id: args.id, organizationId: user.organizationId! },
})
if (!existing) {
throw new GraphQLError('Сотрудник не найден')
}
await prisma.employee.delete({ where: { id: args.id } })
return true
}),
updateEmployeeScheduleV2: withAuth(async (_: unknown, args: any, context: Context) => {
const { employeeId, scheduleData } = args
const user = await checkOrganizationAccess(context.user!.id)
// Проверка что сотрудник принадлежит организации
const employee = await prisma.employee.findFirst({
where: {
id: employeeId,
organizationId: user.organizationId!,
},
})
if (!employee) {
throw new GraphQLError('Сотрудник не найден')
}
// Обновление записей расписания
const scheduleRecords = await Promise.all(
scheduleData.map(async (record: any) => {
return await prisma.employeeSchedule.upsert({
where: {
employeeId_date: {
employeeId: employeeId,
date: record.date,
},
},
create: {
employeeId: employeeId,
date: record.date,
status: record.status,
hoursWorked: record.hoursWorked || 0,
overtimeHours: record.overtimeHours || 0,
notes: record.notes,
},
update: {
status: record.status,
hoursWorked: record.hoursWorked || 0,
overtimeHours: record.overtimeHours || 0,
notes: record.notes,
},
})
}),
)
return {
success: true,
message: 'Расписание сотрудника успешно обновлено',
scheduleRecords: scheduleRecords.map(transformScheduleToV2),
}
}),
}
// =============================================================================
// ЭКСПОРТ РЕЗОЛВЕРОВ
// =============================================================================
export const employeeResolversV2 = {
Query: employeeQueriesV2,
Mutation: employeeMutationsV2,
}

View File

@ -1,6 +0,0 @@
// import type { Context } from '../context'
export const employeeResolvers = {
Query: {},
Mutation: {},
}

View File

@ -1,658 +0,0 @@
import { GraphQLError } from 'graphql'
import { processSupplyOrderReceipt } from '@/lib/inventory-management'
import { prisma } from '@/lib/prisma'
import { notifyOrganization } from '@/lib/realtime'
import { Context } from '../context'
export const fulfillmentConsumableV2Queries = {
myFulfillmentConsumableSupplies: 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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент-центров')
}
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
fulfillmentCenterId: user.organizationId!,
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching fulfillment consumable supplies:', error)
return [] // Возвращаем пустой массив вместо throw
}
},
fulfillmentConsumableSupply: async (_: unknown, args: { 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 supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
// Проверка доступа
if (
user.organization.type === 'FULFILLMENT' &&
supply.fulfillmentCenterId !== user.organizationId
) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (
user.organization.type === 'WHOLESALE' &&
supply.supplierId !== user.organizationId
) {
throw new GraphQLError('Нет доступа к этой поставке')
}
return supply
} catch (error) {
console.error('Error fetching fulfillment consumable supply:', error)
throw new GraphQLError('Ошибка получения поставки')
}
},
// Заявки на поставки для поставщиков (новая система v2)
mySupplierConsumableSupplies: 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 || user.organization.type !== 'WHOLESALE') {
return []
}
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
supplierId: user.organizationId!,
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching supplier consumable supplies:', error)
return []
}
},
}
export const fulfillmentConsumableV2Mutations = {
createFulfillmentConsumableSupply: async (
_: unknown,
args: {
input: {
supplierId: string
requestedDeliveryDate: string
items: Array<{
productId: string
requestedQuantity: number
}>
notes?: 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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент-центры могут создавать поставки расходников')
}
// Проверяем что поставщик существует и является WHOLESALE
const supplier = await prisma.organization.findUnique({
where: { id: args.input.supplierId },
})
if (!supplier || supplier.type !== 'WHOLESALE') {
throw new GraphQLError('Поставщик не найден или не является оптовиком')
}
// Проверяем что все товары существуют и принадлежат поставщику
const productIds = args.input.items.map(item => item.productId)
const products = await prisma.product.findMany({
where: {
id: { in: productIds },
organizationId: supplier.id,
type: 'CONSUMABLE',
},
})
if (products.length !== productIds.length) {
throw new GraphQLError('Некоторые товары не найдены или не принадлежат поставщику')
}
// Создаем поставку с items
const supplyOrder = await prisma.fulfillmentConsumableSupplyOrder.create({
data: {
fulfillmentCenterId: user.organizationId!,
supplierId: supplier.id,
requestedDeliveryDate: new Date(args.input.requestedDeliveryDate),
notes: args.input.notes,
items: {
create: args.input.items.map(item => {
const product = products.find(p => p.id === item.productId)!
return {
productId: item.productId,
requestedQuantity: item.requestedQuantity,
unitPrice: product.price,
totalPrice: product.price.mul(item.requestedQuantity),
}
}),
},
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
// Отправляем уведомление поставщику о новой заявке
await notifyOrganization(supplier.id, {
type: 'supply-order:new',
title: 'Новая заявка на поставку расходников',
message: `Фулфилмент-центр "${user.organization.name}" создал заявку на поставку расходников`,
data: {
supplyOrderId: supplyOrder.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
fulfillmentCenterName: user.organization.name,
itemsCount: args.input.items.length,
requestedDeliveryDate: args.input.requestedDeliveryDate,
},
})
return {
success: true,
message: 'Поставка расходников создана успешно',
supplyOrder,
}
} catch (error) {
console.error('Error creating fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка создания поставки',
supplyOrder: null,
}
}
},
// Одобрение поставки поставщиком
supplierApproveConsumableSupply: async (
_: unknown,
args: { 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 || user.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Только поставщики могут одобрять поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
supplier: true,
fulfillmentCenter: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.supplierId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (supply.status !== 'PENDING') {
throw new GraphQLError('Поставку можно одобрить только в статусе PENDING')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'SUPPLIER_APPROVED',
supplierApprovedAt: new Date(),
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
// Уведомляем фулфилмент-центр об одобрении
await notifyOrganization(supply.fulfillmentCenterId, {
type: 'supply-order:approved',
title: 'Поставка одобрена поставщиком',
message: `Поставщик "${supply.supplier.name}" одобрил заявку на поставку расходников`,
data: {
supplyOrderId: supply.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
supplierName: supply.supplier.name,
},
})
return {
success: true,
message: 'Поставка одобрена успешно',
order: updatedSupply,
}
} catch (error) {
console.error('Error approving fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка одобрения поставки',
order: null,
}
}
},
// Отклонение поставки поставщиком
supplierRejectConsumableSupply: async (
_: unknown,
args: { id: string; reason: 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 || user.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Только поставщики могут отклонять поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
supplier: true,
fulfillmentCenter: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.supplierId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (supply.status !== 'PENDING') {
throw new GraphQLError('Поставку можно отклонить только в статусе PENDING')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'REJECTED',
supplierNotes: args.reason,
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
// Уведомляем фулфилмент-центр об отклонении
await notifyOrganization(supply.fulfillmentCenterId, {
type: 'supply-order:rejected',
title: 'Поставка отклонена поставщиком',
message: `Поставщик "${supply.supplier.name}" отклонил заявку на поставку расходников`,
data: {
supplyOrderId: supply.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
supplierName: supply.supplier.name,
reason: args.reason,
},
})
return {
success: true,
message: 'Поставка отклонена',
order: updatedSupply,
}
} catch (error) {
console.error('Error rejecting fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка отклонения поставки',
order: null,
}
}
},
// Отправка поставки поставщиком
supplierShipConsumableSupply: async (
_: unknown,
args: { id: string; trackingNumber?: 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 || user.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Только поставщики могут отправлять поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
supplier: true,
fulfillmentCenter: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.supplierId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (supply.status !== 'LOGISTICS_CONFIRMED') {
throw new GraphQLError('Поставку можно отправить только в статусе LOGISTICS_CONFIRMED')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'SHIPPED',
shippedAt: new Date(),
trackingNumber: args.trackingNumber,
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
// Уведомляем фулфилмент-центр об отправке
await notifyOrganization(supply.fulfillmentCenterId, {
type: 'supply-order:shipped',
title: 'Поставка отправлена поставщиком',
message: `Поставщик "${supply.supplier.name}" отправил заявку на поставку расходников`,
data: {
supplyOrderId: supply.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
supplierName: supply.supplier.name,
trackingNumber: args.trackingNumber,
},
})
return {
success: true,
message: 'Поставка отправлена',
order: updatedSupply,
}
} catch (error) {
console.error('Error shipping fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка отправки поставки',
order: null,
}
}
},
// Приемка поставки фулфилментом
fulfillmentReceiveConsumableSupply: async (
_: unknown,
args: { id: string; items: Array<{ id: string; receivedQuantity: number; defectQuantity?: number }>; notes?: 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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент-центры могут принимать поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.fulfillmentCenterId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (supply.status !== 'SHIPPED') {
throw new GraphQLError('Поставку можно принять только в статусе SHIPPED')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'DELIVERED',
receivedAt: new Date(),
receivedById: user.id,
receiptNotes: args.notes,
// Обновляем фактические количества товаров
items: {
updateMany: args.items.map(item => ({
where: { id: item.id },
data: {
receivedQuantity: item.receivedQuantity,
defectQuantity: item.defectQuantity || 0,
},
})),
},
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
// Обновляем складские остатки в FulfillmentConsumableInventory
const inventoryItems = args.items.map(item => {
const supplyItem = supply.items.find(si => si.id === item.id)
if (!supplyItem) {
throw new Error(`Supply item not found: ${item.id}`)
}
return {
productId: supplyItem.productId,
receivedQuantity: item.receivedQuantity,
unitPrice: parseFloat(supplyItem.unitPrice.toString()),
}
})
await processSupplyOrderReceipt(supply.id, inventoryItems)
console.log('✅ Inventory updated for supply:', {
supplyId: supply.id,
itemsCount: inventoryItems.length,
totalReceived: inventoryItems.reduce((sum, item) => sum + item.receivedQuantity, 0),
})
// Уведомляем поставщика о приемке
if (supply.supplierId) {
await notifyOrganization(supply.supplierId, {
type: 'supply-order:delivered',
title: 'Поставка принята фулфилментом',
message: `Фулфилмент-центр "${user.organization.name}" принял поставку расходников`,
data: {
supplyOrderId: supply.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
fulfillmentCenterName: user.organization.name,
},
})
}
return {
success: true,
message: 'Поставка успешно принята на склад и остатки обновлены',
order: updatedSupply,
}
} catch (error) {
console.error('Error receiving fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка приемки поставки',
order: null,
}
}
},
}

View File

@ -1,499 +0,0 @@
import { GraphQLError } from 'graphql'
import { prisma } from '@/lib/prisma'
import { notifyOrganization } from '@/lib/realtime'
import { Context } from '../context'
export const fulfillmentConsumableV2Queries = {
myFulfillmentConsumableSupplies: 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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент-центров')
}
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
fulfillmentCenterId: user.organizationId!,
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching fulfillment consumable supplies:', error)
return [] // Возвращаем пустой массив вместо throw
}
},
fulfillmentConsumableSupply: async (_: unknown, args: { 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 supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
// Проверка доступа
if (
user.organization.type === 'FULFILLMENT' &&
supply.fulfillmentCenterId !== user.organizationId
) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (
user.organization.type === 'WHOLESALE' &&
supply.supplierId !== user.organizationId
) {
throw new GraphQLError('Нет доступа к этой поставке')
}
return supply
} catch (error) {
console.error('Error fetching fulfillment consumable supply:', error)
throw new GraphQLError('Ошибка получения поставки')
}
},
// Заявки на поставки для поставщиков (новая система v2)
mySupplierConsumableSupplies: 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 || user.organization.type !== 'WHOLESALE') {
return []
}
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
supplierId: user.organizationId!,
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching supplier consumable supplies:', error)
return []
}
},
}
// =============================================================================
// 🔄 МУТАЦИИ ПОСТАВЩИКА ДЛЯ FULFILLMENT CONSUMABLE SUPPLY
// =============================================================================
const supplierApproveConsumableSupply = async (
_: unknown,
args: { 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 || user.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Только поставщики могут одобрять поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
supplier: true,
fulfillmentCenter: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.supplierId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (supply.status !== 'PENDING') {
throw new GraphQLError('Поставку можно одобрить только в статусе PENDING')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'SUPPLIER_APPROVED',
supplierApprovedAt: new Date(),
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
return {
success: true,
message: 'Поставка одобрена успешно',
order: updatedSupply,
}
} catch (error) {
console.error('Error approving fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка одобрения поставки',
order: null,
}
}
}
const supplierRejectConsumableSupply = async (
_: unknown,
args: { id: string; reason?: 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 || user.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Только поставщики могут отклонять поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
supplier: true,
fulfillmentCenter: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.supplierId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (supply.status !== 'PENDING') {
throw new GraphQLError('Поставку можно отклонить только в статусе PENDING')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'REJECTED',
supplierNotes: args.reason || 'Поставка отклонена',
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
return {
success: true,
message: 'Поставка отклонена',
order: updatedSupply,
}
} catch (error) {
console.error('Error rejecting fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка отклонения поставки',
order: null,
}
}
}
const supplierShipConsumableSupply = async (
_: unknown,
args: { 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 || user.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Только поставщики могут отправлять поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
supplier: true,
fulfillmentCenter: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.supplierId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (!['SUPPLIER_APPROVED', 'LOGISTICS_CONFIRMED'].includes(supply.status)) {
throw new GraphQLError('Поставку можно отправить только в статусе SUPPLIER_APPROVED или LOGISTICS_CONFIRMED')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'SHIPPED',
shippedAt: new Date(),
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
items: {
include: {
product: true,
},
},
},
})
return {
success: true,
message: 'Поставка отправлена',
order: updatedSupply,
}
} catch (error) {
console.error('Error shipping fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка отправки поставки',
order: null,
}
}
}
export const fulfillmentConsumableV2Mutations = {
createFulfillmentConsumableSupply: async (
_: unknown,
args: {
input: {
supplierId: string
requestedDeliveryDate: string
items: Array<{
productId: string
requestedQuantity: number
}>
notes?: 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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент-центры могут создавать поставки расходников')
}
// Проверяем что поставщик существует и является WHOLESALE
const supplier = await prisma.organization.findUnique({
where: { id: args.input.supplierId },
})
if (!supplier || supplier.type !== 'WHOLESALE') {
throw new GraphQLError('Поставщик не найден или не является оптовиком')
}
// Проверяем что все товары существуют и принадлежат поставщику
const productIds = args.input.items.map(item => item.productId)
const products = await prisma.product.findMany({
where: {
id: { in: productIds },
organizationId: supplier.id,
type: 'CONSUMABLE',
},
})
if (products.length !== productIds.length) {
throw new GraphQLError('Некоторые товары не найдены или не принадлежат поставщику')
}
// Создаем поставку с items
const supplyOrder = await prisma.fulfillmentConsumableSupplyOrder.create({
data: {
fulfillmentCenterId: user.organizationId!,
supplierId: supplier.id,
requestedDeliveryDate: new Date(args.input.requestedDeliveryDate),
notes: args.input.notes,
items: {
create: args.input.items.map(item => {
const product = products.find(p => p.id === item.productId)!
return {
productId: item.productId,
requestedQuantity: item.requestedQuantity,
unitPrice: product.price,
totalPrice: product.price.mul(item.requestedQuantity),
}
}),
},
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
// Отправляем уведомление поставщику о новой заявке
await notifyOrganization(supplier.id, {
type: 'supply-order:new',
title: 'Новая заявка на поставку расходников',
message: `Фулфилмент-центр "${user.organization.name}" создал заявку на поставку расходников`,
data: {
supplyOrderId: supplyOrder.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
fulfillmentCenterName: user.organization.name,
itemsCount: args.input.items.length,
requestedDeliveryDate: args.input.requestedDeliveryDate,
},
})
return {
success: true,
message: 'Поставка расходников создана успешно',
supplyOrder,
}
} catch (error) {
console.error('Error creating fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка создания поставки',
supplyOrder: null,
}
}
},
// Добавляем мутации поставщика
supplierApproveConsumableSupply,
supplierRejectConsumableSupply,
supplierShipConsumableSupply,
}

View File

@ -1,132 +0,0 @@
import { GraphQLError } from 'graphql'
import { prisma } from '@/lib/prisma'
import { Context } from '../context'
/**
* НОВЫЙ V2 RESOLVER для складских остатков фулфилмента
*
* Заменяет старый myFulfillmentSupplies резолвер
* Использует новую модель FulfillmentConsumableInventory
* Возвращает данные в формате Supply для совместимости с фронтендом
*/
export const fulfillmentInventoryV2Queries = {
myFulfillmentSupplies: async (_: unknown, __: unknown, context: Context) => {
console.warn('🚀 V2 INVENTORY RESOLVER CALLED')
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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент-центров')
}
// Получаем складские остатки из новой V2 модели
const inventory = await prisma.fulfillmentConsumableInventory.findMany({
where: {
fulfillmentCenterId: user.organizationId || '',
},
include: {
fulfillmentCenter: true,
product: {
include: {
organization: true, // Поставщик товара
},
},
},
orderBy: {
updatedAt: 'desc',
},
})
console.warn('📊 V2 Inventory loaded:', {
fulfillmentCenterId: user.organizationId,
inventoryCount: inventory.length,
items: inventory.map(item => ({
id: item.id,
productName: item.product.name,
currentStock: item.currentStock,
minStock: item.minStock,
})),
})
// Преобразуем V2 данные в формат Supply для совместимости с фронтендом
const suppliesFormatted = inventory.map((item) => {
// Вычисляем статус на основе остатков (используем статус совместимый с фронтендом)
const status = item.currentStock > 0 ? 'На складе' : 'Недоступен'
// Определяем последнего поставщика (пока берем владельца продукта)
const supplier = item.product.organization?.name || 'Неизвестен'
return {
// === ИДЕНТИФИКАЦИЯ (из V2) ===
id: item.id,
productId: item.product.id, // Добавляем productId для фильтрации истории поставок
// === ОСНОВНЫЕ ДАННЫЕ (из Product) ===
name: item.product.name,
article: item.product.article,
description: item.product.description || '',
unit: item.product.unit || 'шт',
category: item.product.category || 'Расходники',
imageUrl: item.product.imageUrl,
// === ЦЕНЫ (из V2) ===
price: parseFloat(item.averageCost.toString()),
pricePerUnit: item.resalePrice ? parseFloat(item.resalePrice.toString()) : null,
// === СКЛАДСКИЕ ДАННЫЕ (из V2) ===
currentStock: item.currentStock,
minStock: item.minStock,
usedStock: item.totalShipped || 0, // Всего использовано (отгружено)
quantity: item.totalReceived, // Всего получено
warehouseStock: item.currentStock, // Дублируем для совместимости
reservedStock: item.reservedStock,
// === ОТГРУЗКИ (из V2) ===
shippedQuantity: item.totalShipped,
totalShipped: item.totalShipped,
// === СТАТУС И МЕТАДАННЫЕ ===
status,
isAvailable: item.currentStock > 0,
supplier,
date: item.lastSupplyDate?.toISOString() || item.createdAt.toISOString(),
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
// === ДОПОЛНИТЕЛЬНЫЕ ПОЛЯ ===
notes: item.notes,
warehouseConsumableId: item.id, // Для совместимости с фронтендом
// === ВЫЧИСЛЯЕМЫЕ ПОЛЯ ===
actualQuantity: item.currentStock, // Фактически доступно
}
})
console.warn('✅ V2 Supplies formatted for frontend:', {
count: suppliesFormatted.length,
totalStock: suppliesFormatted.reduce((sum, item) => sum + item.currentStock, 0),
lowStockItems: suppliesFormatted.filter(item => item.currentStock <= item.minStock).length,
})
return suppliesFormatted
} catch (error) {
console.error('❌ Error in V2 inventory resolver:', error)
// Возвращаем пустой массив вместо ошибки для graceful fallback
return []
}
},
}

View File

@ -1,847 +0,0 @@
// =============================================================================
// 🛠️ РЕЗОЛВЕРЫ ДЛЯ СИСТЕМЫ УСЛУГ ФУЛФИЛМЕНТА V2
// =============================================================================
import { GraphQLError } from 'graphql'
import { prisma } from '../../lib/prisma'
import type { Context } from '../context'
// =============================================================================
// 🔍 QUERY RESOLVERS V2
// =============================================================================
console.warn('🔥 МОДУЛЬ FULFILLMENT-SERVICES-V2 ЗАГРУЖАЕТСЯ')
export const fulfillmentServicesQueries = {
// Мои услуги (для фулфилмента)
myFulfillmentServices: async (_: unknown, __: unknown, context: Context) => {
console.warn('🔍 myFulfillmentServices ВЫЗВАН')
// ИСПРАВЛЕНИЕ: Возвращаем пустой массив вместо ошибки
if (!context.user) {
console.warn('❌ myFulfillmentServices: No user in context')
return []
}
console.warn('✅ context.user найден:', { id: context.user.id })
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
console.warn('👤 User из БД:', {
exists: !!user,
orgExists: !!user?.organization,
orgType: user?.organization?.type,
orgId: user?.organizationId,
})
if (!user?.organization || user.organization.type !== 'FULFILLMENT') {
console.warn('❌ myFulfillmentServices: User is not fulfillment type:', {
hasUser: !!user,
hasOrg: !!user?.organization,
orgType: user?.organization?.type,
})
return []
}
const services = await prisma.fulfillmentService.findMany({
where: {
fulfillmentId: user.organizationId!,
},
include: {
fulfillment: true,
},
orderBy: [
{ sortOrder: 'asc' },
{ name: 'asc' },
],
})
console.warn('myFulfillmentServices: Found', services.length, 'services')
return services
} catch (error) {
console.error('Error fetching fulfillment services:', error)
return []
}
},
// Мои расходники (для фулфилмента)
myFulfillmentConsumables: async (_: unknown, __: unknown, context: Context) => {
console.warn('🚀 myFulfillmentConsumables ВЫЗВАН!')
console.warn('🔍 Context user:', {
exists: !!context.user,
id: context.user?.id,
orgId: context.user?.organizationId,
})
if (!context.user) {
console.warn('❌ myFulfillmentConsumables: No user in context')
return []
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
console.warn('👤 User from DB:', {
exists: !!user,
orgExists: !!user?.organization,
orgType: user?.organization?.type,
fulfillmentId: user?.organizationId,
})
if (!user?.organization || user.organization.type !== 'FULFILLMENT') {
console.warn('❌ myFulfillmentConsumables: User is not fulfillment type')
return []
}
const consumables = await prisma.fulfillmentConsumable.findMany({
where: {
fulfillmentId: user.organizationId!,
},
include: {
fulfillment: true,
inventory: {
include: {
product: true,
},
},
},
orderBy: [
{ sortOrder: 'asc' },
{ name: 'asc' },
],
})
console.warn('✅ myFulfillmentConsumables: Found', consumables.length, 'consumables')
console.warn('📦 Первые 3 записи:', consumables.slice(0, 3).map(c => ({
id: c.id,
name: c.name,
currentStock: c.currentStock,
isAvailable: c.isAvailable,
})))
console.warn('🔥 ВОЗВРАЩАЕМ ДАННЫЕ - длина массива:', consumables.length)
return consumables
} catch (error) {
console.error('❌ ERROR in myFulfillmentConsumables:', error)
return []
}
},
// Моя логистика (для фулфилмента)
myFulfillmentLogistics: async (_: unknown, __: unknown, context: Context) => {
// ИСПРАВЛЕНИЕ: Возвращаем пустой массив вместо ошибки
if (!context.user) {
console.warn('myFulfillmentLogistics: No user in context')
return []
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization || user.organization.type !== 'FULFILLMENT') {
console.warn('myFulfillmentLogistics: User is not fulfillment type')
return []
}
const logistics = await prisma.fulfillmentLogistics.findMany({
where: {
fulfillmentId: user.organizationId!,
},
include: {
fulfillment: true,
},
orderBy: [
{ sortOrder: 'asc' },
{ fromLocation: 'asc' },
],
})
console.warn('myFulfillmentLogistics: Found', logistics.length, 'logistics routes')
return logistics
} catch (error) {
console.error('Error fetching fulfillment logistics:', error)
return []
}
},
// Услуги конкретного фулфилмента (для селлеров при создании поставки)
fulfillmentServicesById: async (_: unknown, args: { fulfillmentId: string }, context: Context) => {
// ИСПРАВЛЕНИЕ: Возвращаем пустой массив вместо ошибки
if (!context.user) {
console.warn('fulfillmentServicesById: No user in context')
return []
}
try {
const services = await prisma.fulfillmentService.findMany({
where: {
fulfillmentId: args.fulfillmentId,
isActive: true,
},
include: {
fulfillment: true,
},
orderBy: [
{ sortOrder: 'asc' },
{ name: 'asc' },
],
})
return services
} catch (error) {
console.error('Error fetching fulfillment services by ID:', error)
return []
}
},
// Расходники конкретного фулфилмента (для селлеров при создании поставки)
fulfillmentConsumablesById: async (_: unknown, args: { fulfillmentId: string }, context: Context) => {
// ИСПРАВЛЕНИЕ: Возвращаем пустой массив вместо ошибки
if (!context.user) {
console.warn('fulfillmentConsumablesById: No user in context')
return []
}
try {
const consumables = await prisma.fulfillmentConsumable.findMany({
where: {
fulfillmentId: args.fulfillmentId,
isAvailable: true,
},
include: {
fulfillment: true,
},
orderBy: [
{ sortOrder: 'asc' },
{ name: 'asc' },
],
})
return consumables
} catch (error) {
console.error('Error fetching fulfillment consumables by ID:', error)
return []
}
},
}
// =============================================================================
// 🔧 MUTATION RESOLVERS V2
// =============================================================================
interface CreateFulfillmentServiceInput {
name: string
description?: string
price: number
unit?: string
imageUrl?: string
sortOrder?: number
}
interface UpdateFulfillmentServiceInput {
id: string
name?: string
description?: string
price?: number
unit?: string
imageUrl?: string
sortOrder?: number
isActive?: boolean
}
interface CreateFulfillmentConsumableInput {
name: string
article?: string
description?: string
pricePerUnit: number
unit?: string
minStock?: number
currentStock?: number
imageUrl?: string
sortOrder?: number
}
interface UpdateFulfillmentConsumableInput {
id: string
name?: string
nameForSeller?: string
article?: string
pricePerUnit?: number
unit?: string
minStock?: number
currentStock?: number
imageUrl?: string
sortOrder?: number
isAvailable?: boolean
}
interface CreateFulfillmentLogisticsInput {
fromLocation: string
toLocation: string
fromAddress?: string
toAddress?: string
priceUnder1m3: number
priceOver1m3: number
estimatedDays: number
description?: string
sortOrder?: number
}
interface UpdateFulfillmentLogisticsInput {
id: string
fromLocation?: string
toLocation?: string
fromAddress?: string
toAddress?: string
priceUnder1m3?: number
priceOver1m3?: number
estimatedDays?: number
description?: string
sortOrder?: number
isActive?: boolean
}
export const fulfillmentServicesMutations = {
// =============================================================================
// 🔧 МУТАЦИИ ДЛЯ УСЛУГ ФУЛФИЛМЕНТА V2
// =============================================================================
// Создание услуги
createFulfillmentService: async (
_: unknown,
args: { input: CreateFulfillmentServiceInput },
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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент может создавать услуги')
}
const service = await prisma.fulfillmentService.create({
data: {
fulfillmentId: user.organizationId!,
name: args.input.name,
description: args.input.description,
price: args.input.price,
unit: args.input.unit || 'шт',
imageUrl: args.input.imageUrl,
sortOrder: args.input.sortOrder || 0,
isActive: true,
},
include: {
fulfillment: true,
},
})
return {
success: true,
message: 'Услуга успешно создана',
service,
}
} catch (error: any) {
console.error('Error creating fulfillment service:', error)
return {
success: false,
message: `Ошибка при создании услуги: ${error.message}`,
service: null,
}
}
},
// Обновление услуги
updateFulfillmentService: async (
_: unknown,
args: { input: UpdateFulfillmentServiceInput },
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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент может обновлять услуги')
}
// Проверяем что услуга принадлежит текущему фулфилменту
const existingService = await prisma.fulfillmentService.findFirst({
where: {
id: args.input.id,
fulfillmentId: user.organizationId!,
},
})
if (!existingService) {
return {
success: false,
message: 'Услуга не найдена или не принадлежит вашей организации',
service: null,
}
}
const updateData: any = {}
if (args.input.name !== undefined) updateData.name = args.input.name
if (args.input.description !== undefined) updateData.description = args.input.description
if (args.input.price !== undefined) updateData.price = args.input.price
if (args.input.unit !== undefined) updateData.unit = args.input.unit
if (args.input.imageUrl !== undefined) updateData.imageUrl = args.input.imageUrl
if (args.input.sortOrder !== undefined) updateData.sortOrder = args.input.sortOrder
if (args.input.isActive !== undefined) updateData.isActive = args.input.isActive
const service = await prisma.fulfillmentService.update({
where: { id: args.input.id },
data: updateData,
include: {
fulfillment: true,
},
})
return {
success: true,
message: 'Услуга успешно обновлена',
service,
}
} catch (error: any) {
console.error('Error updating fulfillment service:', error)
return {
success: false,
message: `Ошибка при обновлении услуги: ${error.message}`,
service: null,
}
}
},
// Удаление услуги
deleteFulfillmentService: async (_: unknown, args: { 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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент может удалять услуги')
}
// Проверяем что услуга принадлежит текущему фулфилменту
const existingService = await prisma.fulfillmentService.findFirst({
where: {
id: args.id,
fulfillmentId: user.organizationId!,
},
})
if (!existingService) {
throw new GraphQLError('Услуга не найдена или не принадлежит вашей организации')
}
await prisma.fulfillmentService.delete({
where: { id: args.id },
})
return true
} catch (error: any) {
console.error('Error deleting fulfillment service:', error)
throw new GraphQLError(`Ошибка при удалении услуги: ${error.message}`)
}
},
// =============================================================================
// 📦 МУТАЦИИ ДЛЯ РАСХОДНИКОВ ФУЛФИЛМЕНТА V2
// =============================================================================
// Создание расходника
createFulfillmentConsumable: async (
_: unknown,
args: { input: CreateFulfillmentConsumableInput },
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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент может создавать расходники')
}
const consumable = await prisma.fulfillmentConsumable.create({
data: {
fulfillmentId: user.organizationId!,
name: args.input.name,
article: args.input.article,
description: args.input.description,
pricePerUnit: args.input.pricePerUnit,
unit: args.input.unit || 'шт',
minStock: args.input.minStock || 0,
currentStock: args.input.currentStock || 0,
isAvailable: (args.input.currentStock || 0) > 0,
imageUrl: args.input.imageUrl,
sortOrder: args.input.sortOrder || 0,
},
include: {
fulfillment: true,
},
})
return {
success: true,
message: 'Расходник успешно создан',
consumable,
}
} catch (error: any) {
console.error('Error creating fulfillment consumable:', error)
return {
success: false,
message: `Ошибка при создании расходника: ${error.message}`,
consumable: null,
}
}
},
// Обновление расходника
updateFulfillmentConsumable: async (
_: unknown,
args: { input: UpdateFulfillmentConsumableInput },
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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент может обновлять расходники')
}
// Проверяем что расходник принадлежит текущему фулфилменту
const existingConsumable = await prisma.fulfillmentConsumable.findFirst({
where: {
id: args.input.id,
fulfillmentId: user.organizationId!,
},
})
if (!existingConsumable) {
return {
success: false,
message: 'Расходник не найден или не принадлежит вашей организации',
consumable: null,
}
}
const updateData: any = {}
if (args.input.name !== undefined) updateData.name = args.input.name
if (args.input.nameForSeller !== undefined) updateData.nameForSeller = args.input.nameForSeller
if (args.input.article !== undefined) updateData.article = args.input.article
if (args.input.pricePerUnit !== undefined) updateData.pricePerUnit = args.input.pricePerUnit
if (args.input.unit !== undefined) updateData.unit = args.input.unit
if (args.input.minStock !== undefined) updateData.minStock = args.input.minStock
if (args.input.currentStock !== undefined) {
updateData.currentStock = args.input.currentStock
updateData.isAvailable = args.input.currentStock > 0
}
if (args.input.imageUrl !== undefined) updateData.imageUrl = args.input.imageUrl
if (args.input.sortOrder !== undefined) updateData.sortOrder = args.input.sortOrder
if (args.input.isAvailable !== undefined) updateData.isAvailable = args.input.isAvailable
const consumable = await prisma.fulfillmentConsumable.update({
where: { id: args.input.id },
data: updateData,
include: {
fulfillment: true,
},
})
return {
success: true,
message: 'Расходник успешно обновлен',
consumable,
}
} catch (error: any) {
console.error('Error updating fulfillment consumable:', error)
return {
success: false,
message: `Ошибка при обновлении расходника: ${error.message}`,
consumable: null,
}
}
},
// Удаление расходника
deleteFulfillmentConsumable: async (_: unknown, args: { 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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент может удалять расходники')
}
// Проверяем что расходник принадлежит текущему фулфилменту
const existingConsumable = await prisma.fulfillmentConsumable.findFirst({
where: {
id: args.id,
fulfillmentId: user.organizationId!,
},
})
if (!existingConsumable) {
throw new GraphQLError('Расходник не найден или не принадлежит вашей организации')
}
await prisma.fulfillmentConsumable.delete({
where: { id: args.id },
})
return true
} catch (error: any) {
console.error('Error deleting fulfillment consumable:', error)
throw new GraphQLError(`Ошибка при удалении расходника: ${error.message}`)
}
},
// =============================================================================
// 🚚 МУТАЦИИ ДЛЯ ЛОГИСТИКИ ФУЛФИЛМЕНТА V2
// =============================================================================
// Создание логистического маршрута
createFulfillmentLogistics: async (
_: unknown,
args: { input: CreateFulfillmentLogisticsInput },
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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент может создавать логистические маршруты')
}
const logistics = await prisma.fulfillmentLogistics.create({
data: {
fulfillmentId: user.organizationId!,
fromLocation: args.input.fromLocation,
toLocation: args.input.toLocation,
fromAddress: args.input.fromAddress,
toAddress: args.input.toAddress,
priceUnder1m3: args.input.priceUnder1m3,
priceOver1m3: args.input.priceOver1m3,
estimatedDays: args.input.estimatedDays,
description: args.input.description,
isActive: true,
sortOrder: args.input.sortOrder || 0,
},
include: {
fulfillment: true,
},
})
return {
success: true,
message: 'Логистический маршрут успешно создан',
logistics,
}
} catch (error: any) {
console.error('Error creating fulfillment logistics:', error)
return {
success: false,
message: `Ошибка при создании логистического маршрута: ${error.message}`,
logistics: null,
}
}
},
// Обновление логистического маршрута
updateFulfillmentLogistics: async (
_: unknown,
args: { input: UpdateFulfillmentLogisticsInput },
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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент может обновлять логистические маршруты')
}
// Проверяем что маршрут принадлежит текущему фулфилменту
const existingLogistics = await prisma.fulfillmentLogistics.findFirst({
where: {
id: args.input.id,
fulfillmentId: user.organizationId!,
},
})
if (!existingLogistics) {
return {
success: false,
message: 'Логистический маршрут не найден или не принадлежит вашей организации',
logistics: null,
}
}
const updateData: any = {}
if (args.input.fromLocation !== undefined) updateData.fromLocation = args.input.fromLocation
if (args.input.toLocation !== undefined) updateData.toLocation = args.input.toLocation
if (args.input.fromAddress !== undefined) updateData.fromAddress = args.input.fromAddress
if (args.input.toAddress !== undefined) updateData.toAddress = args.input.toAddress
if (args.input.priceUnder1m3 !== undefined) updateData.priceUnder1m3 = args.input.priceUnder1m3
if (args.input.priceOver1m3 !== undefined) updateData.priceOver1m3 = args.input.priceOver1m3
if (args.input.estimatedDays !== undefined) updateData.estimatedDays = args.input.estimatedDays
if (args.input.description !== undefined) updateData.description = args.input.description
if (args.input.sortOrder !== undefined) updateData.sortOrder = args.input.sortOrder
if (args.input.isActive !== undefined) updateData.isActive = args.input.isActive
const logistics = await prisma.fulfillmentLogistics.update({
where: { id: args.input.id },
data: updateData,
include: {
fulfillment: true,
},
})
return {
success: true,
message: 'Логистический маршрут успешно обновлен',
logistics,
}
} catch (error: any) {
console.error('Error updating fulfillment logistics:', error)
return {
success: false,
message: `Ошибка при обновлению логистического маршрута: ${error.message}`,
logistics: null,
}
}
},
// Удаление логистического маршрута
deleteFulfillmentLogistics: async (_: unknown, args: { 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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент может удалять логистические маршруты')
}
// Проверяем что маршрут принадлежит текущему фулфилменту
const existingLogistics = await prisma.fulfillmentLogistics.findFirst({
where: {
id: args.id,
fulfillmentId: user.organizationId!,
},
})
if (!existingLogistics) {
throw new GraphQLError('Логистический маршрут не найден или не принадлежит вашей организации')
}
await prisma.fulfillmentLogistics.delete({
where: { id: args.id },
})
return true
} catch (error: any) {
console.error('Error deleting fulfillment logistics:', error)
throw new GraphQLError(`Ошибка при удалении логистического маршрута: ${error.message}`)
}
},
}
console.warn('🔥 FULFILLMENT QUERIES ОБЪЕКТ СОЗДАН:', {
keys: Object.keys(fulfillmentServicesQueries),
hasMyFulfillmentConsumables: 'myFulfillmentConsumables' in fulfillmentServicesQueries,
})
// Объединяем резолверы в основной объект
export const fulfillmentServicesV2Resolvers = {
Query: fulfillmentServicesQueries,
Mutation: fulfillmentServicesMutations,
}
console.warn('🔥 МОДУЛЬ FULFILLMENT-SERVICES-V2 ЭКСПОРТЫ ГОТОВЫ')

View File

@ -1,753 +0,0 @@
// =============================================================================
// 🛒 РЕЗОЛВЕРЫ ДЛЯ СИСТЕМЫ ПОСТАВОК ТОВАРОВ СЕЛЛЕРА V2
// =============================================================================
import { GraphQLError } from 'graphql'
import { processSellerGoodsSupplyReceipt } from '@/lib/inventory-management-goods'
import { prisma } from '@/lib/prisma'
import { notifyOrganization } from '@/lib/realtime'
import { Context } from '../context'
// =============================================================================
// 🔍 QUERY RESOLVERS V2
// =============================================================================
export const sellerGoodsQueries = {
// Мои товарные поставки (для селлеров - заказы которые я создал)
mySellerGoodsSupplies: 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 || user.organization.type !== 'SELLER') {
return []
}
const supplies = await prisma.sellerGoodsSupplyOrder.findMany({
where: {
sellerId: user.organizationId!,
},
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
recipeItems: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching seller goods supplies:', error)
return []
}
},
// Входящие товарные заказы от селлеров (для фулфилмента)
incomingSellerGoodsSupplies: 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 || user.organization.type !== 'FULFILLMENT') {
return []
}
const supplies = await prisma.sellerGoodsSupplyOrder.findMany({
where: {
fulfillmentCenterId: user.organizationId!,
},
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
recipeItems: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching incoming seller goods supplies:', error)
return []
}
},
// Товарные заказы от селлеров (для поставщиков)
mySellerGoodsSupplyRequests: 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 || user.organization.type !== 'WHOLESALE') {
return []
}
const supplies = await prisma.sellerGoodsSupplyOrder.findMany({
where: {
supplierId: user.organizationId!,
},
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
recipeItems: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching seller goods supply requests:', error)
return []
}
},
// Получение конкретной товарной поставки селлера
sellerGoodsSupply: async (_: unknown, args: { 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 supply = await prisma.sellerGoodsSupplyOrder.findUnique({
where: { id: args.id },
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
recipeItems: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
// Проверка доступа
const hasAccess =
(user.organization.type === 'SELLER' && supply.sellerId === user.organizationId) ||
(user.organization.type === 'FULFILLMENT' && supply.fulfillmentCenterId === user.organizationId) ||
(user.organization.type === 'WHOLESALE' && supply.supplierId === user.organizationId) ||
(user.organization.type === 'LOGIST' && supply.logisticsPartnerId === user.organizationId)
if (!hasAccess) {
throw new GraphQLError('Нет доступа к этой поставке')
}
return supply
} catch (error) {
console.error('Error fetching seller goods supply:', error)
if (error instanceof GraphQLError) {
throw error
}
throw new GraphQLError('Ошибка получения товарной поставки')
}
},
// Инвентарь товаров селлера на складе фулфилмента
mySellerGoodsInventory: 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) {
return []
}
let inventoryItems
if (user.organization.type === 'SELLER') {
// Селлер видит свои товары на всех складах
inventoryItems = await prisma.sellerGoodsInventory.findMany({
where: {
sellerId: user.organizationId!,
},
include: {
seller: true,
fulfillmentCenter: true,
product: true,
},
orderBy: {
lastSupplyDate: 'desc',
},
})
} else if (user.organization.type === 'FULFILLMENT') {
// Фулфилмент видит все товары на своем складе
inventoryItems = await prisma.sellerGoodsInventory.findMany({
where: {
fulfillmentCenterId: user.organizationId!,
},
include: {
seller: true,
fulfillmentCenter: true,
product: true,
},
orderBy: {
lastSupplyDate: 'desc',
},
})
} else {
return []
}
return inventoryItems
} catch (error) {
console.error('Error fetching seller goods inventory:', error)
return []
}
},
}
// =============================================================================
// ✏️ MUTATION RESOLVERS V2
// =============================================================================
export const sellerGoodsMutations = {
// Создание поставки товаров селлера
createSellerGoodsSupply: async (_: unknown, args: { input: any }, 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 || user.organization.type !== 'SELLER') {
throw new GraphQLError('Доступно только для селлеров')
}
const { fulfillmentCenterId, supplierId, logisticsPartnerId, requestedDeliveryDate, notes, recipeItems } = args.input
// 🔍 ВАЛИДАЦИЯ ПАРТНЕРОВ
// Проверяем фулфилмент-центр
const fulfillmentCenter = await prisma.organization.findUnique({
where: { id: fulfillmentCenterId },
include: {
counterpartyOf: {
where: { organizationId: user.organizationId! },
},
},
})
if (!fulfillmentCenter || fulfillmentCenter.type !== 'FULFILLMENT') {
throw new GraphQLError('Фулфилмент-центр не найден или имеет неверный тип')
}
if (fulfillmentCenter.counterpartyOf.length === 0) {
throw new GraphQLError('Нет партнерских отношений с данным фулфилмент-центром')
}
// Проверяем поставщика
const supplier = await prisma.organization.findUnique({
where: { id: supplierId },
include: {
counterpartyOf: {
where: { organizationId: user.organizationId! },
},
},
})
if (!supplier || supplier.type !== 'WHOLESALE') {
throw new GraphQLError('Поставщик не найден или имеет неверный тип')
}
if (supplier.counterpartyOf.length === 0) {
throw new GraphQLError('Нет партнерских отношений с данным поставщиком')
}
// 🔍 ВАЛИДАЦИЯ ТОВАРОВ И ОСТАТКОВ
let totalCost = 0
const mainProducts = recipeItems.filter((item: any) => item.recipeType === 'MAIN_PRODUCT')
if (mainProducts.length === 0) {
throw new GraphQLError('Должен быть хотя бы один основной товар')
}
// Проверяем только основные товары (MAIN_PRODUCT) в рецептуре
for (const item of recipeItems) {
// В V2 временно валидируем только основные товары
if (item.recipeType !== 'MAIN_PRODUCT') {
console.log(`⚠️ Пропускаем валидацию ${item.recipeType} товара ${item.productId} - не поддерживается в V2`)
continue
}
const product = await prisma.product.findUnique({
where: { id: item.productId },
})
if (!product) {
throw new GraphQLError(`Товар с ID ${item.productId} не найден`)
}
if (product.organizationId !== supplierId) {
throw new GraphQLError(`Товар ${product.name} не принадлежит выбранному поставщику`)
}
// Проверяем остатки основных товаров
const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0)
if (item.quantity > availableStock) {
throw new GraphQLError(
`Недостаточно остатков товара "${product.name}". ` +
`Доступно: ${availableStock} шт., запрашивается: ${item.quantity} шт.`,
)
}
totalCost += product.price.toNumber() * item.quantity
}
// 🚀 СОЗДАНИЕ ПОСТАВКИ В ТРАНЗАКЦИИ
const supplyOrder = await prisma.$transaction(async (tx) => {
// Создаем заказ поставки
const newOrder = await tx.sellerGoodsSupplyOrder.create({
data: {
sellerId: user.organizationId!,
fulfillmentCenterId,
supplierId,
logisticsPartnerId,
requestedDeliveryDate: new Date(requestedDeliveryDate),
notes,
status: 'PENDING',
totalCostWithDelivery: totalCost,
},
})
// Создаем записи рецептуры только для MAIN_PRODUCT
for (const item of recipeItems) {
// В V2 временно создаем только основные товары
if (item.recipeType !== 'MAIN_PRODUCT') {
console.log(`⚠️ Пропускаем создание записи для ${item.recipeType} товара ${item.productId}`)
continue
}
await tx.goodsSupplyRecipeItem.create({
data: {
supplyOrderId: newOrder.id,
productId: item.productId,
quantity: item.quantity,
recipeType: item.recipeType,
},
})
// Резервируем основные товары у поставщика
await tx.product.update({
where: { id: item.productId },
data: {
ordered: {
increment: item.quantity,
},
},
})
}
return newOrder
})
// 📨 УВЕДОМЛЕНИЯ
await notifyOrganization(
supplierId,
`Новый заказ товаров от селлера ${user.organization.name}`,
'GOODS_SUPPLY_ORDER_CREATED',
{ orderId: supplyOrder.id },
)
await notifyOrganization(
fulfillmentCenterId,
`Селлер ${user.organization.name} оформил поставку товаров на ваш склад`,
'INCOMING_GOODS_SUPPLY_ORDER',
{ orderId: supplyOrder.id },
)
// Получаем созданную поставку с полными данными
const createdSupply = await prisma.sellerGoodsSupplyOrder.findUnique({
where: { id: supplyOrder.id },
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
recipeItems: {
include: {
product: true,
},
},
},
})
return {
success: true,
message: 'Поставка товаров успешно создана',
supplyOrder: createdSupply,
}
} catch (error) {
console.error('Error creating seller goods supply:', error)
if (error instanceof GraphQLError) {
throw error
}
throw new GraphQLError('Ошибка создания товарной поставки')
}
},
// Обновление статуса товарной поставки
updateSellerGoodsSupplyStatus: async (
_: unknown,
args: { id: string; status: string; notes?: string },
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация')
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
const supply = await prisma.sellerGoodsSupplyOrder.findUnique({
where: { id: args.id },
include: {
seller: true,
supplier: true,
fulfillmentCenter: true,
recipeItems: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
// 🔐 ПРОВЕРКА ПРАВ И ЛОГИКИ ПЕРЕХОДОВ СТАТУСОВ
const { status } = args
const currentStatus = supply.status
const orgType = user.organization.type
// Только поставщики могут переводить PENDING → APPROVED
if (status === 'APPROVED' && currentStatus === 'PENDING') {
if (orgType !== 'WHOLESALE' || supply.supplierId !== user.organizationId) {
throw new GraphQLError('Только поставщик может одобрить заказ')
}
}
// Только поставщики могут переводить APPROVED → SHIPPED
else if (status === 'SHIPPED' && currentStatus === 'APPROVED') {
if (orgType !== 'WHOLESALE' || supply.supplierId !== user.organizationId) {
throw new GraphQLError('Только поставщик может отметить отгрузку')
}
}
// Только фулфилмент может переводить SHIPPED → DELIVERED
else if (status === 'DELIVERED' && currentStatus === 'SHIPPED') {
if (orgType !== 'FULFILLMENT' || supply.fulfillmentCenterId !== user.organizationId) {
throw new GraphQLError('Только фулфилмент-центр может подтвердить получение')
}
}
// Только фулфилмент может переводить DELIVERED → COMPLETED
else if (status === 'COMPLETED' && currentStatus === 'DELIVERED') {
if (orgType !== 'FULFILLMENT' || supply.fulfillmentCenterId !== user.organizationId) {
throw new GraphQLError('Только фулфилмент-центр может завершить поставку')
}
} else {
throw new GraphQLError('Недопустимый переход статуса')
}
// 📅 ОБНОВЛЕНИЕ ВРЕМЕННЫХ МЕТОК
const updateData: any = {
status,
updatedAt: new Date(),
}
if (status === 'APPROVED' && orgType === 'WHOLESALE') {
updateData.supplierApprovedAt = new Date()
updateData.supplierNotes = args.notes
}
if (status === 'SHIPPED' && orgType === 'WHOLESALE') {
updateData.shippedAt = new Date()
}
if (status === 'DELIVERED' && orgType === 'FULFILLMENT') {
updateData.deliveredAt = new Date()
updateData.receivedById = user.id
updateData.receiptNotes = args.notes
}
// 🔄 ОБНОВЛЕНИЕ В БАЗЕ
const updatedSupply = await prisma.sellerGoodsSupplyOrder.update({
where: { id: args.id },
data: updateData,
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
recipeItems: {
include: {
product: true,
},
},
},
})
// 📨 УВЕДОМЛЕНИЯ О СМЕНЕ СТАТУСА
if (status === 'APPROVED') {
await notifyOrganization(
supply.sellerId,
`Поставка товаров одобрена поставщиком ${user.organization.name}`,
'GOODS_SUPPLY_APPROVED',
{ orderId: args.id },
)
}
if (status === 'SHIPPED') {
await notifyOrganization(
supply.sellerId,
`Поставка товаров отгружена поставщиком ${user.organization.name}`,
'GOODS_SUPPLY_SHIPPED',
{ orderId: args.id },
)
await notifyOrganization(
supply.fulfillmentCenterId,
'Поставка товаров в пути. Ожидается доставка',
'GOODS_SUPPLY_IN_TRANSIT',
{ orderId: args.id },
)
}
if (status === 'DELIVERED') {
// 📦 АВТОМАТИЧЕСКОЕ СОЗДАНИЕ/ОБНОВЛЕНИЕ ИНВЕНТАРЯ V2
await processSellerGoodsSupplyReceipt(args.id)
await notifyOrganization(
supply.sellerId,
`Поставка товаров доставлена в ${supply.fulfillmentCenter.name}`,
'GOODS_SUPPLY_DELIVERED',
{ orderId: args.id },
)
}
if (status === 'COMPLETED') {
await notifyOrganization(
supply.sellerId,
`Поставка товаров завершена. Товары размещены на складе ${supply.fulfillmentCenter.name}`,
'GOODS_SUPPLY_COMPLETED',
{ orderId: args.id },
)
}
return updatedSupply
} catch (error) {
console.error('Error updating seller goods supply status:', error)
if (error instanceof GraphQLError) {
throw error
}
throw new GraphQLError('Ошибка обновления статуса товарной поставки')
}
},
// Отмена товарной поставки селлером
cancelSellerGoodsSupply: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация')
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization || user.organization.type !== 'SELLER') {
throw new GraphQLError('Только селлеры могут отменять свои поставки')
}
const supply = await prisma.sellerGoodsSupplyOrder.findUnique({
where: { id: args.id },
include: {
seller: true,
recipeItems: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.sellerId !== user.organizationId) {
throw new GraphQLError('Вы можете отменить только свои поставки')
}
// ✅ ПРОВЕРКА ВОЗМОЖНОСТИ ОТМЕНЫ (только PENDING и APPROVED)
if (!['PENDING', 'APPROVED'].includes(supply.status)) {
throw new GraphQLError('Поставку можно отменить только в статусе PENDING или APPROVED')
}
// 🔄 ОТМЕНА В ТРАНЗАКЦИИ
const cancelledSupply = await prisma.$transaction(async (tx) => {
// Обновляем статус
const updated = await tx.sellerGoodsSupplyOrder.update({
where: { id: args.id },
data: {
status: 'CANCELLED',
updatedAt: new Date(),
},
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
recipeItems: {
include: {
product: true,
},
},
},
})
// Освобождаем зарезервированные товары у поставщика (только MAIN_PRODUCT)
for (const item of supply.recipeItems) {
if (item.recipeType === 'MAIN_PRODUCT') {
await tx.product.update({
where: { id: item.productId },
data: {
ordered: {
decrement: item.quantity,
},
},
})
}
}
return updated
})
// 📨 УВЕДОМЛЕНИЯ ОБ ОТМЕНЕ
if (supply.supplierId) {
await notifyOrganization(
supply.supplierId,
`Селлер ${user.organization.name} отменил заказ товаров`,
'GOODS_SUPPLY_CANCELLED',
{ orderId: args.id },
)
}
await notifyOrganization(
supply.fulfillmentCenterId,
`Селлер ${user.organization.name} отменил поставку товаров`,
'GOODS_SUPPLY_CANCELLED',
{ orderId: args.id },
)
return cancelledSupply
} catch (error) {
console.error('Error cancelling seller goods supply:', error)
if (error instanceof GraphQLError) {
throw error
}
throw new GraphQLError('Ошибка отмены товарной поставки')
}
},
}

View File

@ -1,7 +1,7 @@
// import { resolvers as oldResolvers } from '../resolvers' // LEGACY: Монолитный файл больше не используется
import { JSONScalar, DateTimeScalar } from '../scalars'
import { authResolvers } from './auth'
import { authResolvers } from './domains/auth'
import { cartResolvers } from './domains/cart'
import { catalogResolvers } from './domains/catalog'
import { messagingResolvers } from './domains/messaging'
@ -24,13 +24,9 @@ import { adminToolsResolvers } from './domains/admin-tools'
import { fileManagementResolvers } from './domains/file-management'
import { externalAdsResolvers } from './domains/external-ads'
import { sellerConsumablesResolvers } from './domains/seller-consumables'
import { employeeResolvers } from './employees'
// V2 импорты удалены - заменены на доменные резолверы
import { logisticsResolvers } from './logistics'
import { referralResolvers } from './referrals'
// import { integrateSecurityWithExistingResolvers } from './secure-integration' // ВРЕМЕННО ОТКЛЮЧЕНО из-за экспорта
// import { secureSuppliesResolvers } from './secure-supplies'
import { sellerConsumableQueries, sellerConsumableMutations } from './seller-consumables'
// import { suppliesResolvers } from './supplies' // ЗАМЕНЕН на domains/supplies
// Типы для резолверов
@ -122,9 +118,6 @@ const mergedResolvers = mergeResolvers(
fileManagementResolvers,
externalAdsResolvers,
sellerConsumablesResolvers,
// employeeResolvers, // старый V1
// logisticsResolvers, // ЗАМЕНЕН на logisticsDomainResolvers
// referralResolvers, // ЗАМЕНЕН на referralDomainResolvers
// БЕЗОПАСНЫЕ резолверы поставок - ВРЕМЕННО ОТКЛЮЧЕН из-за ошибки импорта
// secureSuppliesResolvers,

View File

@ -1,262 +0,0 @@
import { GraphQLError } from 'graphql'
import { prisma } from '@/lib/prisma'
import { notifyOrganization } from '@/lib/realtime'
import { Context } from '../context'
export const logisticsConsumableV2Queries = {
// Получить V2 поставки расходников для логистической компании
myLogisticsConsumableSupplies: 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 || user.organization.type !== 'LOGIST') {
return []
}
// Получаем поставки где назначена наша логистическая компания
// или поставки в статусе SUPPLIER_APPROVED (ожидают назначения логистики)
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
OR: [
// Поставки назначенные нашей логистической компании
{
logisticsPartnerId: user.organizationId!,
},
// Поставки в статусе SUPPLIER_APPROVED (доступные для назначения)
{
status: 'SUPPLIER_APPROVED',
logisticsPartnerId: null,
},
],
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching logistics consumable supplies:', error)
return []
}
},
}
export const logisticsConsumableV2Mutations = {
// Подтверждение поставки логистикой
logisticsConfirmConsumableSupply: async (
_: unknown,
args: { 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 || user.organization.type !== 'LOGIST') {
throw new GraphQLError('Только логистические компании могут подтверждать поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
// Проверяем права доступа
if (supply.logisticsPartnerId && supply.logisticsPartnerId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
// Проверяем статус - может подтвердить SUPPLIER_APPROVED или назначить себя
if (!['SUPPLIER_APPROVED'].includes(supply.status)) {
throw new GraphQLError('Поставку можно подтвердить только в статусе SUPPLIER_APPROVED')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'LOGISTICS_CONFIRMED',
logisticsPartnerId: user.organizationId, // Назначаем себя если не назначены
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
items: {
include: {
product: true,
},
},
},
})
// Уведомляем фулфилмент-центр о подтверждении логистикой
await notifyOrganization(supply.fulfillmentCenterId, {
type: 'supply-order:logistics-confirmed',
title: 'Логистика подтверждена',
message: `Логистическая компания "${user.organization.name}" подтвердила поставку расходников`,
data: {
supplyOrderId: supply.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
logisticsCompanyName: user.organization.name,
},
})
// Уведомляем поставщика
if (supply.supplierId) {
await notifyOrganization(supply.supplierId, {
type: 'supply-order:logistics-confirmed',
title: 'Логистика подтверждена',
message: `Логистическая компания "${user.organization.name}" подтвердила поставку`,
data: {
supplyOrderId: supply.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
logisticsCompanyName: user.organization.name,
},
})
}
return {
success: true,
message: 'Поставка подтверждена логистикой',
order: updatedSupply,
}
} catch (error) {
console.error('Error confirming logistics consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка подтверждения поставки',
order: null,
}
}
},
// Отклонение поставки логистикой
logisticsRejectConsumableSupply: async (
_: unknown,
args: { id: string; reason?: 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 || user.organization.type !== 'LOGIST') {
throw new GraphQLError('Только логистические компании могут отклонять поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
// Проверяем права доступа
if (supply.logisticsPartnerId && supply.logisticsPartnerId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (!['SUPPLIER_APPROVED', 'LOGISTICS_CONFIRMED'].includes(supply.status)) {
throw new GraphQLError('Поставку можно отклонить только в статусе SUPPLIER_APPROVED или LOGISTICS_CONFIRMED')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'LOGISTICS_REJECTED',
logisticsNotes: args.reason,
logisticsPartnerId: null, // Убираем назначение
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
items: {
include: {
product: true,
},
},
},
})
// Уведомляем фулфилмент-центр об отклонении
await notifyOrganization(supply.fulfillmentCenterId, {
type: 'supply-order:logistics-rejected',
title: 'Поставка отклонена логистикой',
message: `Логистическая компания "${user.organization.name}" отклонила поставку расходников`,
data: {
supplyOrderId: supply.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
logisticsCompanyName: user.organization.name,
reason: args.reason,
},
})
return {
success: true,
message: 'Поставка отклонена логистикой',
order: updatedSupply,
}
} catch (error) {
console.error('Error rejecting logistics consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка отклонения поставки',
order: null,
}
}
},
}

View File

@ -1,272 +0,0 @@
import { GraphQLError } from 'graphql'
import { prisma } from '../../lib/prisma'
import { Context } from '../context'
export const logisticsResolvers = {
Query: {
// Получить логистические компании-партнеры
logisticsPartners: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
// Получаем все организации типа LOGIST
return await prisma.organization.findMany({
where: {
type: 'LOGIST',
// Убираем фильтр по статусу пока не определим правильные значения
},
orderBy: { createdAt: 'desc' }, // Сортируем по дате создания вместо name
})
},
},
Mutation: {
// Назначить логистику на поставку (используется фулфилментом)
assignLogisticsToSupply: async (
_: unknown,
args: {
supplyOrderId: string
logisticsPartnerId: string
responsibleId?: string
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент центров')
}
try {
// Находим заказ
const existingOrder = await prisma.supplyOrder.findUnique({
where: { id: args.supplyOrderId },
include: {
partner: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: { product: true },
},
},
})
if (!existingOrder) {
throw new GraphQLError('Заказ поставки не найден')
}
// Проверяем, что это заказ для нашего фулфилмент-центра
if (existingOrder.fulfillmentCenterId !== currentUser.organization.id) {
throw new GraphQLError('Нет доступа к этому заказу')
}
// Проверяем, что статус позволяет назначить логистику
if (existingOrder.status !== 'SUPPLIER_APPROVED') {
throw new GraphQLError(`Нельзя назначить логистику для заказа со статусом ${existingOrder.status}`)
}
// Проверяем, что логистическая компания существует
const logisticsPartner = await prisma.organization.findUnique({
where: { id: args.logisticsPartnerId },
})
if (!logisticsPartner || logisticsPartner.type !== 'LOGIST') {
throw new GraphQLError('Логистическая компания не найдена')
}
// Обновляем заказ
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.supplyOrderId },
data: {
logisticsPartner: {
connect: { id: args.logisticsPartnerId },
},
status: 'CONFIRMED', // Переводим в статус "подтвержден фулфилментом"
},
include: {
partner: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: { product: true },
},
},
})
console.warn(`✅ Логистика назначена на заказ ${args.supplyOrderId}:`, {
logisticsPartner: logisticsPartner.name,
responsible: args.responsibleId,
newStatus: 'CONFIRMED',
})
return {
success: true,
message: 'Логистика успешно назначена',
order: updatedOrder,
}
} catch (error) {
console.error('❌ Ошибка при назначении логистики:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка при назначении логистики',
}
}
},
// Подтвердить заказ логистической компанией
logisticsConfirmOrder: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
const existingOrder = await prisma.supplyOrder.findFirst({
where: {
id: args.id,
logisticsPartnerId: currentUser.organization.id,
OR: [{ status: 'SUPPLIER_APPROVED' }, { status: 'CONFIRMED' }],
},
})
if (!existingOrder) {
return {
success: false,
message: 'Заказ не найден или недоступен для подтверждения логистикой',
}
}
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.id },
data: { status: 'LOGISTICS_CONFIRMED' },
include: {
partner: true,
organization: true,
fulfillmentCenter: true,
logisticsPartner: true,
items: {
include: {
product: {
include: {
category: true,
organization: true,
},
},
},
},
},
})
return {
success: true,
message: 'Заказ подтвержден логистической компанией',
order: updatedOrder,
}
} catch (error) {
console.error('Error confirming supply order:', error)
return {
success: false,
message: 'Ошибка при подтверждении заказа',
}
}
},
// Отклонить заказ логистической компанией
logisticsRejectOrder: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
try {
const existingOrder = await prisma.supplyOrder.findFirst({
where: {
id: args.id,
logisticsPartnerId: currentUser.organization.id,
OR: [{ status: 'SUPPLIER_APPROVED' }, { status: 'CONFIRMED' }],
},
})
if (!existingOrder) {
return {
success: false,
message: 'Заказ не найден или недоступен для отклонения логистикой',
}
}
const updatedOrder = await prisma.supplyOrder.update({
where: { id: args.id },
data: {
status: 'CANCELLED',
logisticsPartnerId: null, // Убираем назначенную логистику
},
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 rejecting supply order:', error)
return {
success: false,
message: 'Ошибка при отклонении заказа',
}
}
},
},
}

View File

@ -1,107 +0,0 @@
import { GraphQLError } from 'graphql'
import { prisma } from '@/lib/prisma'
interface Context {
user: {
id: string
phone?: string
organizationId?: string
organization?: {
id: string
type: string
}
} | null
}
export const referralResolvers = {
Query: {
// Получить реферальную ссылку текущего пользователя
myReferralLink: async (_: unknown, __: unknown, context: Context) => {
if (!context.user?.organizationId) {
return 'http://localhost:3000/register?ref=PLEASE_LOGIN'
}
const organization = await prisma.organization.findUnique({
where: { id: context.user.organizationId },
select: { referralCode: true },
})
if (!organization?.referralCode) {
throw new GraphQLError('Реферальный код не найден')
}
const link = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/register?ref=${organization.referralCode}`
return link || 'http://localhost:3000/register?ref=ERROR'
},
// Получить партнерскую ссылку текущего пользователя
myPartnerLink: async (_: unknown, __: unknown, _context: Context) => {
if (!context.user?.organizationId) {
throw new GraphQLError('Требуется авторизация и организация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const organization = await prisma.organization.findUnique({
where: { id: context.user.organizationId },
select: { referralCode: true },
})
if (!organization?.referralCode) {
throw new GraphQLError('Реферальный код не найден')
}
const link = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/register?partner=${organization.referralCode}`
return link
},
// Получить статистику по рефералам
myReferralStats: async (_: unknown, __: unknown, _context: Context) => {
// Простая заглушка для устранения ошибки 500
return {
totalPartners: 0,
totalSpheres: 0,
monthlyPartners: 0,
monthlySpheres: 0,
referralsByType: [
{ type: 'SELLER', count: 0, spheres: 0 },
{ type: 'WHOLESALE', count: 0, spheres: 0 },
{ type: 'FULFILLMENT', count: 0, spheres: 0 },
{ type: 'LOGIST', count: 0, spheres: 0 },
],
referralsBySource: [
{ source: 'REFERRAL_LINK', count: 0, spheres: 0 },
{ source: 'AUTO_BUSINESS', count: 0, spheres: 0 },
],
}
},
// Получить список рефералов
myReferrals: async (_: unknown, _args: unknown, context: Context) => {
if (!context.user?.organizationId) {
throw new GraphQLError('Требуется авторизация и организация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
// Временная заглушка для отладки
const result = {
referrals: [],
totalCount: 0,
totalPages: 0,
}
return result
} catch {
return {
referrals: [],
totalCount: 0,
totalPages: 0,
}
}
},
},
}

View File

@ -1,735 +0,0 @@
// =============================================================================
// 📦 РЕЗОЛВЕРЫ ДЛЯ СИСТЕМЫ ПОСТАВОК РАСХОДНИКОВ СЕЛЛЕРА
// =============================================================================
import { GraphQLError } from 'graphql'
import { processSellerConsumableSupplyReceipt } from '@/lib/inventory-management'
import { prisma } from '@/lib/prisma'
import { notifyOrganization } from '@/lib/realtime'
import { Context } from '../context'
// =============================================================================
// 🔍 QUERY RESOLVERS
// =============================================================================
export const sellerConsumableQueries = {
// Мои поставки (для селлеров - заказы которые я создал)
mySellerConsumableSupplies: 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 || user.organization.type !== 'SELLER') {
return [] // Возвращаем пустой массив если пользователь не селлер
}
const supplies = await prisma.sellerConsumableSupplyOrder.findMany({
where: {
sellerId: user.organizationId!,
},
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching seller consumable supplies:', error)
return [] // Возвращаем пустой массив вместо throw
}
},
// Входящие заказы от селлеров (для фулфилмента - заказы в мой ФФ)
incomingSellerSupplies: 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 || user.organization.type !== 'FULFILLMENT') {
return [] // Доступно только для фулфилмент-центров
}
const supplies = await prisma.sellerConsumableSupplyOrder.findMany({
where: {
fulfillmentCenterId: user.organizationId!,
},
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching incoming seller supplies:', error)
return []
}
},
// Заказы от селлеров (для поставщиков - заказы которые нужно выполнить)
mySellerSupplyRequests: 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 || user.organization.type !== 'WHOLESALE') {
return [] // Доступно только для поставщиков
}
const supplies = await prisma.sellerConsumableSupplyOrder.findMany({
where: {
supplierId: user.organizationId!,
},
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching seller supply requests:', error)
return []
}
},
// Получение конкретной поставки селлера
sellerConsumableSupply: async (_: unknown, args: { 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 supply = await prisma.sellerConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
// Проверка доступа в зависимости от типа организации
const hasAccess =
(user.organization.type === 'SELLER' && supply.sellerId === user.organizationId) ||
(user.organization.type === 'FULFILLMENT' && supply.fulfillmentCenterId === user.organizationId) ||
(user.organization.type === 'WHOLESALE' && supply.supplierId === user.organizationId) ||
(user.organization.type === 'LOGIST' && supply.logisticsPartnerId === user.organizationId)
if (!hasAccess) {
throw new GraphQLError('Нет доступа к этой поставке')
}
return supply
} catch (error) {
console.error('Error fetching seller consumable supply:', error)
throw new GraphQLError('Ошибка получения поставки')
}
},
}
// =============================================================================
// ✏️ MUTATION RESOLVERS
// =============================================================================
export const sellerConsumableMutations = {
// Создание поставки расходников селлера
createSellerConsumableSupply: async (_: unknown, args: { input: any }, 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 || user.organization.type !== 'SELLER') {
throw new GraphQLError('Доступно только для селлеров')
}
const { fulfillmentCenterId, supplierId, logisticsPartnerId, requestedDeliveryDate, items, notes } = args.input
// 🔍 ВАЛИДАЦИЯ ПАРТНЕРОВ
// Проверяем что фулфилмент-центр существует и является партнером
const fulfillmentCenter = await prisma.organization.findUnique({
where: { id: fulfillmentCenterId },
include: {
counterpartyOf: {
where: { organizationId: user.organizationId! },
},
},
})
if (!fulfillmentCenter || fulfillmentCenter.type !== 'FULFILLMENT') {
throw new GraphQLError('Фулфилмент-центр не найден или имеет неверный тип')
}
if (fulfillmentCenter.counterpartyOf.length === 0) {
throw new GraphQLError('Нет партнерских отношений с данным фулфилмент-центром')
}
// Проверяем поставщика
const supplier = await prisma.organization.findUnique({
where: { id: supplierId },
include: {
counterpartyOf: {
where: { organizationId: user.organizationId! },
},
},
})
if (!supplier || supplier.type !== 'WHOLESALE') {
throw new GraphQLError('Поставщик не найден или имеет неверный тип')
}
if (supplier.counterpartyOf.length === 0) {
throw new GraphQLError('Нет партнерских отношений с данным поставщиком')
}
// 🔍 ВАЛИДАЦИЯ ТОВАРОВ И ОСТАТКОВ
let totalAmount = 0
const validatedItems = []
for (const item of items) {
const product = await prisma.product.findUnique({
where: { id: item.productId },
})
if (!product) {
throw new GraphQLError(`Товар с ID ${item.productId} не найден`)
}
if (product.organizationId !== supplierId) {
throw new GraphQLError(`Товар ${product.name} не принадлежит выбранному поставщику`)
}
if (product.type !== 'CONSUMABLE') {
throw new GraphQLError(`Товар ${product.name} не является расходником`)
}
// ✅ ПРОВЕРКА ОСТАТКОВ У ПОСТАВЩИКА
const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0)
if (item.requestedQuantity > availableStock) {
throw new GraphQLError(
`Недостаточно остатков товара "${product.name}". ` +
`Доступно: ${availableStock} шт., запрашивается: ${item.requestedQuantity} шт.`,
)
}
const itemTotalPrice = product.price.toNumber() * item.requestedQuantity
totalAmount += itemTotalPrice
validatedItems.push({
productId: item.productId,
requestedQuantity: item.requestedQuantity,
unitPrice: product.price,
totalPrice: itemTotalPrice,
})
}
// 🚀 СОЗДАНИЕ ПОСТАВКИ В ТРАНЗАКЦИИ
const supplyOrder = await prisma.$transaction(async (tx) => {
// Создаем заказ поставки
const newOrder = await tx.sellerConsumableSupplyOrder.create({
data: {
sellerId: user.organizationId!,
fulfillmentCenterId,
supplierId,
logisticsPartnerId,
requestedDeliveryDate: new Date(requestedDeliveryDate),
notes,
status: 'PENDING',
totalCostWithDelivery: totalAmount,
},
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
items: {
include: {
product: true,
},
},
},
})
// Создаем позиции заказа
for (const item of validatedItems) {
await tx.sellerConsumableSupplyItem.create({
data: {
supplyOrderId: newOrder.id,
...item,
},
})
// Резервируем товар у поставщика (увеличиваем ordered)
await tx.product.update({
where: { id: item.productId },
data: {
ordered: {
increment: item.requestedQuantity,
},
},
})
}
return newOrder
})
// 📨 УВЕДОМЛЕНИЯ
// Уведомляем поставщика о новом заказе
await notifyOrganization(supplierId, `Новый заказ от селлера ${user.organization.name}`, 'SUPPLY_ORDER_CREATED', {
orderId: supplyOrder.id,
})
// Уведомляем фулфилмент о входящей поставке
await notifyOrganization(
fulfillmentCenterId,
`Селлер ${user.organization.name} оформил поставку на ваш склад`,
'INCOMING_SUPPLY_ORDER',
{ orderId: supplyOrder.id },
)
return {
success: true,
message: 'Поставка успешно создана',
supplyOrder: await prisma.sellerConsumableSupplyOrder.findUnique({
where: { id: supplyOrder.id },
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
}),
}
} catch (error) {
console.error('Error creating seller consumable supply:', error)
if (error instanceof GraphQLError) {
throw error
}
throw new GraphQLError('Ошибка создания поставки')
}
},
// Обновление статуса поставки (для поставщиков и фулфилмента)
updateSellerSupplyStatus: async (
_: unknown,
args: { id: string; status: string; notes?: string },
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация')
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization) {
throw new GraphQLError('Организация не найдена')
}
const supply = await prisma.sellerConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
seller: true,
supplier: true,
fulfillmentCenter: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
// 🔐 ПРОВЕРКА ПРАВ И ЛОГИКИ ПЕРЕХОДОВ СТАТУСОВ
const { status } = args
const currentStatus = supply.status
const orgType = user.organization.type
// Только поставщики могут переводить PENDING → APPROVED
if (status === 'APPROVED' && currentStatus === 'PENDING') {
if (orgType !== 'WHOLESALE' || supply.supplierId !== user.organizationId) {
throw new GraphQLError('Только поставщик может одобрить заказ')
}
}
// Только поставщики могут переводить APPROVED → SHIPPED
else if (status === 'SHIPPED' && currentStatus === 'APPROVED') {
if (orgType !== 'WHOLESALE' || supply.supplierId !== user.organizationId) {
throw new GraphQLError('Только поставщик может отметить отгрузку')
}
}
// Только фулфилмент может переводить SHIPPED → DELIVERED
else if (status === 'DELIVERED' && currentStatus === 'SHIPPED') {
if (orgType !== 'FULFILLMENT' || supply.fulfillmentCenterId !== user.organizationId) {
throw new GraphQLError('Только фулфилмент-центр может подтвердить получение')
}
}
// Только фулфилмент может переводить DELIVERED → COMPLETED
else if (status === 'COMPLETED' && currentStatus === 'DELIVERED') {
if (orgType !== 'FULFILLMENT' || supply.fulfillmentCenterId !== user.organizationId) {
throw new GraphQLError('Только фулфилмент-центр может завершить поставку')
}
} else {
throw new GraphQLError('Недопустимый переход статуса')
}
// 📅 ОБНОВЛЕНИЕ ВРЕМЕННЫХ МЕТОК
const updateData: any = {
status,
updatedAt: new Date(),
}
if (status === 'APPROVED' && orgType === 'WHOLESALE') {
updateData.supplierApprovedAt = new Date()
updateData.supplierNotes = args.notes
}
if (status === 'SHIPPED' && orgType === 'WHOLESALE') {
updateData.shippedAt = new Date()
}
if (status === 'DELIVERED' && orgType === 'FULFILLMENT') {
updateData.deliveredAt = new Date()
updateData.receivedById = user.id
updateData.receiptNotes = args.notes
}
// 🔄 ОБНОВЛЕНИЕ В БАЗЕ
const updatedSupply = await prisma.sellerConsumableSupplyOrder.update({
where: { id: args.id },
data: updateData,
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
})
// 📨 УВЕДОМЛЕНИЯ О СМЕНЕ СТАТУСА
if (status === 'APPROVED') {
await notifyOrganization(
supply.sellerId,
`Поставка одобрена поставщиком ${user.organization.name}`,
'SUPPLY_APPROVED',
{ orderId: args.id },
)
}
if (status === 'SHIPPED') {
await notifyOrganization(
supply.sellerId,
`Поставка отгружена поставщиком ${user.organization.name}`,
'SUPPLY_SHIPPED',
{ orderId: args.id },
)
await notifyOrganization(
supply.fulfillmentCenterId,
'Поставка в пути. Ожидается доставка',
'SUPPLY_IN_TRANSIT',
{ orderId: args.id },
)
}
if (status === 'DELIVERED') {
// 📦 АВТОМАТИЧЕСКОЕ ПОПОЛНЕНИЕ ИНВЕНТАРЯ V2
const inventoryItems = updatedSupply.items.map(item => ({
productId: item.productId,
receivedQuantity: item.quantity,
unitPrice: parseFloat(item.price.toString()),
}))
await processSellerConsumableSupplyReceipt(args.id, inventoryItems)
await notifyOrganization(
supply.sellerId,
`Поставка доставлена в ${supply.fulfillmentCenter.name}`,
'SUPPLY_DELIVERED',
{ orderId: args.id },
)
}
if (status === 'COMPLETED') {
// 📦 V2: СОЗДАНИЕ РАСХОДНИКОВ НА СКЛАДЕ ФУЛФИЛМЕНТА
for (const item of updatedSupply.items) {
// V2: Используем SellerConsumableInventory вместо Supply
await prisma.sellerConsumableInventory.upsert({
where: {
sellerId_fulfillmentCenterId_productId: {
sellerId: supply.sellerId,
fulfillmentCenterId: supply.fulfillmentCenterId,
productId: item.productId,
}
},
update: {
// При повторной поставке увеличиваем остаток
currentStock: {
increment: item.receivedQuantity || item.requestedQuantity,
},
totalReceived: {
increment: item.receivedQuantity || item.requestedQuantity,
},
lastSupplyDate: new Date(),
updatedAt: new Date(),
},
create: {
sellerId: supply.sellerId, // ✅ Владелец - селлер
fulfillmentCenterId: supply.fulfillmentCenterId, // ✅ Хранитель - фулфилмент
productId: item.productId, // ✅ Связь с продуктом
currentStock: item.receivedQuantity || item.requestedQuantity,
minStock: 0, // Настраивается селлером
totalReceived: item.receivedQuantity || item.requestedQuantity,
totalUsed: 0,
reservedStock: 0,
lastSupplyDate: new Date(),
notes: `V2: Поступление от поставки ${supply.id} (Legacy migration)`,
},
})
console.log('✅ LEGACY V2 MIGRATION: SellerConsumableInventory record created/updated', {
sellerId: supply.sellerId,
fulfillmentCenterId: supply.fulfillmentCenterId,
productId: item.productId,
quantity: item.receivedQuantity || item.requestedQuantity,
})
}
await notifyOrganization(
supply.sellerId,
`Поставка завершена. Расходники размещены на складе ${supply.fulfillmentCenter.name}`,
'SUPPLY_COMPLETED',
{ orderId: args.id },
)
}
return updatedSupply
} catch (error) {
console.error('Error updating seller supply status:', error)
if (error instanceof GraphQLError) {
throw error
}
throw new GraphQLError('Ошибка обновления статуса поставки')
}
},
// Отмена поставки селлером (только PENDING/APPROVED)
cancelSellerSupply: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация')
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization || user.organization.type !== 'SELLER') {
throw new GraphQLError('Только селлеры могут отменять свои поставки')
}
const supply = await prisma.sellerConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
seller: true,
items: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.sellerId !== user.organizationId) {
throw new GraphQLError('Вы можете отменить только свои поставки')
}
// ✅ ПРОВЕРКА ВОЗМОЖНОСТИ ОТМЕНЫ (только PENDING и APPROVED)
if (!['PENDING', 'APPROVED'].includes(supply.status)) {
throw new GraphQLError('Поставку можно отменить только в статусе PENDING или APPROVED')
}
// 🔄 ОТМЕНА В ТРАНЗАКЦИИ
const cancelledSupply = await prisma.$transaction(async (tx) => {
// Обновляем статус
const updated = await tx.sellerConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'CANCELLED',
updatedAt: new Date(),
},
include: {
seller: true,
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
// Освобождаем зарезервированные товары у поставщика
for (const item of supply.items) {
await tx.product.update({
where: { id: item.productId },
data: {
ordered: {
decrement: item.requestedQuantity,
},
},
})
}
return updated
})
// 📨 УВЕДОМЛЕНИЯ ОБ ОТМЕНЕ
if (supply.supplierId) {
await notifyOrganization(
supply.supplierId,
`Селлер ${user.organization.name} отменил заказ`,
'SUPPLY_CANCELLED',
{ orderId: args.id },
)
}
await notifyOrganization(
supply.fulfillmentCenterId,
`Селлер ${user.organization.name} отменил поставку`,
'SUPPLY_CANCELLED',
{ orderId: args.id },
)
return cancelledSupply
} catch (error) {
console.error('Error cancelling seller supply:', error)
if (error instanceof GraphQLError) {
throw error
}
throw new GraphQLError('Ошибка отмены поставки')
}
},
}

View File

@ -1,238 +0,0 @@
import { GraphQLError } from 'graphql'
import { prisma } from '@/lib/prisma'
import { Context } from '../context'
/**
* НОВЫЙ V2 RESOLVER для складских остатков расходников селлера
*
* Управляет расходниками селлера, хранящимися на складе фулфилмента
* Использует новую модель SellerConsumableInventory
* Возвращает данные в формате Supply для совместимости с фронтендом
*/
export const sellerInventoryV2Queries = {
/**
* Расходники селлера на складе фулфилмента (для селлера)
*/
mySellerConsumableInventory: async (_: unknown, __: unknown, context: Context) => {
console.warn('🚀 V2 SELLER INVENTORY RESOLVER CALLED')
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 || user.organization.type !== 'SELLER') {
throw new GraphQLError('Доступно только для селлеров')
}
// Получаем складские остатки расходников селлера из V2 модели
const inventory = await prisma.sellerConsumableInventory.findMany({
where: {
sellerId: user.organizationId || '',
},
include: {
seller: true,
fulfillmentCenter: true,
product: {
include: {
organization: true, // Поставщик товара
},
},
},
orderBy: {
updatedAt: 'desc',
},
})
console.warn('📊 V2 Seller Inventory loaded:', {
sellerId: user.organizationId,
inventoryCount: inventory.length,
items: inventory.map(item => ({
id: item.id,
productName: item.product.name,
currentStock: item.currentStock,
minStock: item.minStock,
fulfillmentCenter: item.fulfillmentCenter.name,
})),
})
// Преобразуем V2 данные в формат Supply для совместимости с фронтендом
const suppliesFormatted = inventory.map((item) => {
// Вычисляем статус на основе остатков
const status = item.currentStock > 0 ? 'На складе' : 'Недоступен'
// Определяем поставщика
const supplier = item.product.organization?.name || 'Неизвестен'
return {
// === ИДЕНТИФИКАЦИЯ (из V2) ===
id: item.id,
productId: item.product.id,
// === ОСНОВНЫЕ ДАННЫЕ (из Product) ===
name: item.product.name,
article: item.product.article,
description: item.product.description || '',
unit: item.product.unit || 'шт',
category: item.product.category || 'Расходники',
imageUrl: item.product.imageUrl,
// === ЦЕНЫ (из V2) ===
price: parseFloat(item.averageCost.toString()),
pricePerUnit: item.usagePrice ? parseFloat(item.usagePrice.toString()) : null,
// === СКЛАДСКИЕ ДАННЫЕ (из V2) ===
currentStock: item.currentStock,
minStock: item.minStock,
usedStock: item.totalUsed || 0, // Всего использовано
quantity: item.totalReceived, // Всего получено
warehouseStock: item.currentStock, // Дублируем для совместимости
reservedStock: item.reservedStock,
// === ИСПОЛЬЗОВАНИЕ (из V2) ===
shippedQuantity: item.totalUsed,
totalShipped: item.totalUsed,
// === СТАТУС И МЕТАДАННЫЕ ===
status,
isAvailable: item.currentStock > 0,
supplier,
date: item.lastSupplyDate?.toISOString() || item.createdAt.toISOString(),
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
// === ДОПОЛНИТЕЛЬНЫЕ ПОЛЯ ===
notes: item.notes,
warehouseConsumableId: item.id,
fulfillmentCenter: item.fulfillmentCenter.name, // Где хранится
// === ВЫЧИСЛЯЕМЫЕ ПОЛЯ ===
actualQuantity: item.currentStock, // Фактически доступно
}
})
console.warn('✅ V2 Seller Supplies formatted for frontend:', {
count: suppliesFormatted.length,
totalStock: suppliesFormatted.reduce((sum, item) => sum + item.currentStock, 0),
lowStockItems: suppliesFormatted.filter(item => item.currentStock <= item.minStock).length,
})
return suppliesFormatted
} catch (error) {
console.error('❌ Error in V2 seller inventory resolver:', error)
// Возвращаем пустой массив вместо ошибки для graceful fallback
return []
}
},
/**
* Расходники всех селлеров на складе фулфилмента (для фулфилмента)
*/
allSellerConsumableInventory: async (_: unknown, __: unknown, context: Context) => {
console.warn('🚀 V2 ALL SELLER INVENTORY RESOLVER CALLED')
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 || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент-центров')
}
// Получаем складские остатки всех селлеров на нашем складе
const inventory = await prisma.sellerConsumableInventory.findMany({
where: {
fulfillmentCenterId: user.organizationId || '',
},
include: {
seller: true,
fulfillmentCenter: true,
product: {
include: {
organization: true, // Поставщик товара
},
},
},
orderBy: [
{ seller: { name: 'asc' } }, // Группируем по селлерам
{ updatedAt: 'desc' },
],
})
console.warn('📊 V2 All Seller Inventory loaded for fulfillment:', {
fulfillmentCenterId: user.organizationId,
inventoryCount: inventory.length,
uniqueSellers: new Set(inventory.map(item => item.sellerId)).size,
})
// Возвращаем данные сгруппированные по селлерам для таблицы "Детализация по магазинам"
return inventory.map((item) => {
const status = item.currentStock > 0 ? 'На складе' : 'Недоступен'
const supplier = item.product.organization?.name || 'Неизвестен'
return {
// === ИДЕНТИФИКАЦИЯ ===
id: item.id,
productId: item.product.id,
sellerId: item.sellerId,
sellerName: item.seller.name,
// === ОСНОВНЫЕ ДАННЫЕ ===
name: item.product.name,
article: item.product.article,
description: item.product.description || '',
unit: item.product.unit || 'шт',
category: item.product.category || 'Расходники',
imageUrl: item.product.imageUrl,
// === СКЛАДСКИЕ ДАННЫЕ ===
currentStock: item.currentStock,
minStock: item.minStock,
usedStock: item.totalUsed || 0,
quantity: item.totalReceived,
reservedStock: item.reservedStock,
// === ЦЕНЫ ===
price: parseFloat(item.averageCost.toString()),
pricePerUnit: item.usagePrice ? parseFloat(item.usagePrice.toString()) : null,
// === МЕТАДАННЫЕ ===
status,
isAvailable: item.currentStock > 0,
supplier,
date: item.lastSupplyDate?.toISOString() || item.createdAt.toISOString(),
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
notes: item.notes,
// === СПЕЦИФИЧНЫЕ ПОЛЯ ДЛЯ ФУЛФИЛМЕНТА ===
warehouseConsumableId: item.id,
actualQuantity: item.currentStock,
}
})
} catch (error) {
console.error('❌ Error in V2 all seller inventory resolver:', error)
return []
}
},
}

View File

@ -1,6 +0,0 @@
// import type { Context } from '../context'
export const suppliesResolvers = {
Query: {},
Mutation: {},
}