diff --git a/back-button-variants.html b/back-button-variants.html deleted file mode 100644 index f1a188f..0000000 --- a/back-button-variants.html +++ /dev/null @@ -1,263 +0,0 @@ - - - - - - Варианты кнопки "Назад" - - - - -

Варианты размещения кнопки "Назад"

- - -
-

🌟 Вариант 1: Плавающая кнопка слева

-
- - - - -
- -
- - -
-
-
- - - -

Поставщики

-
-
- Поиск поставщиков... -
-
-
-
-
- - -
-

📍 Вариант 2: Полоса навигации сверху

-
- - - - -
- -
- -
- - -
-
-
- - - -

Поставщики

-
-
- Поиск поставщиков... -
-
-
-
-
-
- - -
-

🎯 Вариант 3: Кнопка в разрыве

-
- - - - -
- -
- - -
-
-
- - - -

Поставщики

-
-
- Поиск поставщиков... -
-
-
-
-
- - -
-

🍞 Вариант 4: Breadcrumb стиль

-
- - - - -
- -
- - - Поставщики -
- - -
-
-
- - - -

Поставщики

-
-
- Поиск поставщиков... -
-
-
-
-
-
- - -
-

🔥 Вариант 5: Вертикальная панель

-
- - - - -
- -
- - -
-
-
- - - -

Поставщики

-
-
- Поиск поставщиков... -
-
-
-
-
- -
-

Наведите курсор на кнопки для просмотра hover эффектов

-
- - \ No newline at end of file diff --git a/diagnostic-script.js b/diagnostic-script.js deleted file mode 100644 index 575bb70..0000000 --- a/diagnostic-script.js +++ /dev/null @@ -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(); diff --git a/current-session.md b/legacy-rules/current-session.md similarity index 100% rename from current-session.md rename to legacy-rules/current-session.md diff --git a/seller_supply_migration.sql b/seller_supply_migration.sql deleted file mode 100644 index 60116cc..0000000 --- a/seller_supply_migration.sql +++ /dev/null @@ -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 'Система поставок расходников селлера успешно создана!'; \ No newline at end of file diff --git a/server.log b/server.log index 056e62d..85ee65d 100644 --- a/server.log +++ b/server.log @@ -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 (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 (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 () + at new Promise () + at new Promise () + 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 () + at new Promise () + at new Promise () + 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. (.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. (.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 diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts deleted file mode 100644 index c177808..0000000 --- a/src/graphql/resolvers.ts +++ /dev/null @@ -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 => { - 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 = { - // Больше не исключаем собственную организацию - } - - 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 = { - 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 = { - 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 = {} - 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 = {} - 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}\nДата доставки: ${new Date(args.input.deliveryDate).toLocaleDateString( - 'ru-RU', - )}\nОбщая сумма: ${totalAmount.toLocaleString( - 'ru-RU', - )} ₽\n\nПожалуйста, подтвердите заказ в разделе "Поставки".` - - await prisma.message.create({ - data: { - content: notificationMessage, - type: 'TEXT', - senderId: context.user.id, - senderOrganizationId: currentUser.organization.id, - receiverOrganizationId: args.input.partnerId, - }, - }) - - console.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 diff --git a/src/graphql/resolvers/auth.ts b/src/graphql/resolvers/domains/auth.ts similarity index 100% rename from src/graphql/resolvers/auth.ts rename to src/graphql/resolvers/domains/auth.ts diff --git a/src/graphql/resolvers/domains/inventory.ts b/src/graphql/resolvers/domains/inventory.ts index 67fbde8..0f129ef 100644 --- a/src/graphql/resolvers/domains/inventory.ts +++ b/src/graphql/resolvers/domains/inventory.ts @@ -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, diff --git a/src/graphql/resolvers/employees-v2.ts b/src/graphql/resolvers/employees-v2.ts deleted file mode 100644 index 37c0ba5..0000000 --- a/src/graphql/resolvers/employees-v2.ts +++ /dev/null @@ -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, -} \ No newline at end of file diff --git a/src/graphql/resolvers/employees.ts b/src/graphql/resolvers/employees.ts deleted file mode 100644 index 9c6ba7d..0000000 --- a/src/graphql/resolvers/employees.ts +++ /dev/null @@ -1,6 +0,0 @@ -// import type { Context } from '../context' - -export const employeeResolvers = { - Query: {}, - Mutation: {}, -} diff --git a/src/graphql/resolvers/fulfillment-consumables-v2-restored.ts b/src/graphql/resolvers/fulfillment-consumables-v2-restored.ts deleted file mode 100644 index 604892f..0000000 --- a/src/graphql/resolvers/fulfillment-consumables-v2-restored.ts +++ /dev/null @@ -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, - } - } - }, -} \ No newline at end of file diff --git a/src/graphql/resolvers/fulfillment-consumables-v2.ts b/src/graphql/resolvers/fulfillment-consumables-v2.ts deleted file mode 100644 index 0e66495..0000000 --- a/src/graphql/resolvers/fulfillment-consumables-v2.ts +++ /dev/null @@ -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, -} \ No newline at end of file diff --git a/src/graphql/resolvers/fulfillment-inventory-v2.ts b/src/graphql/resolvers/fulfillment-inventory-v2.ts deleted file mode 100644 index 700234f..0000000 --- a/src/graphql/resolvers/fulfillment-inventory-v2.ts +++ /dev/null @@ -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 [] - } - }, -} \ No newline at end of file diff --git a/src/graphql/resolvers/fulfillment-services-v2.ts b/src/graphql/resolvers/fulfillment-services-v2.ts deleted file mode 100644 index 4bfc924..0000000 --- a/src/graphql/resolvers/fulfillment-services-v2.ts +++ /dev/null @@ -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 ЭКСПОРТЫ ГОТОВЫ') \ No newline at end of file diff --git a/src/graphql/resolvers/goods-supply-v2.ts b/src/graphql/resolvers/goods-supply-v2.ts deleted file mode 100644 index 2839c1a..0000000 --- a/src/graphql/resolvers/goods-supply-v2.ts +++ /dev/null @@ -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('Ошибка отмены товарной поставки') - } - }, -} \ No newline at end of file diff --git a/src/graphql/resolvers/index.ts b/src/graphql/resolvers/index.ts index 4926b4b..6b88bc1 100644 --- a/src/graphql/resolvers/index.ts +++ b/src/graphql/resolvers/index.ts @@ -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, diff --git a/src/graphql/resolvers/logistics-consumables-v2.ts b/src/graphql/resolvers/logistics-consumables-v2.ts deleted file mode 100644 index 5d34fe0..0000000 --- a/src/graphql/resolvers/logistics-consumables-v2.ts +++ /dev/null @@ -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, - } - } - }, -} \ No newline at end of file diff --git a/src/graphql/resolvers/logistics.ts b/src/graphql/resolvers/logistics.ts deleted file mode 100644 index d8a8406..0000000 --- a/src/graphql/resolvers/logistics.ts +++ /dev/null @@ -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: 'Ошибка при отклонении заказа', - } - } - }, - }, -} diff --git a/src/graphql/resolvers/referrals.ts b/src/graphql/resolvers/referrals.ts deleted file mode 100644 index 3739e57..0000000 --- a/src/graphql/resolvers/referrals.ts +++ /dev/null @@ -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, - } - } - }, - }, -} diff --git a/src/graphql/resolvers/seller-consumables.ts b/src/graphql/resolvers/seller-consumables.ts deleted file mode 100644 index e738d66..0000000 --- a/src/graphql/resolvers/seller-consumables.ts +++ /dev/null @@ -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('Ошибка отмены поставки') - } - }, -} diff --git a/src/graphql/resolvers/seller-inventory-v2.ts b/src/graphql/resolvers/seller-inventory-v2.ts deleted file mode 100644 index a212b3f..0000000 --- a/src/graphql/resolvers/seller-inventory-v2.ts +++ /dev/null @@ -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 [] - } - }, -} \ No newline at end of file diff --git a/src/graphql/resolvers/supplies.ts b/src/graphql/resolvers/supplies.ts deleted file mode 100644 index 0b53637..0000000 --- a/src/graphql/resolvers/supplies.ts +++ /dev/null @@ -1,6 +0,0 @@ -// import type { Context } from '../context' - -export const suppliesResolvers = { - Query: {}, - Mutation: {}, -}