Files
sfera-new/src/graphql/resolvers/domains/inventory.ts
Veronika Smirnova 2269de6c85 feat: завершить миграцию на доменно-модульную архитектуру резолверов
🏗️ КРУПНОЕ РЕФАКТОРИНГ: Полный переход от монолитной к доменной архитектуре

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

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

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

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 17:07:32 +03:00

1179 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import { 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,
withAuth,
withOrgTypeAuth
} from '../shared/auth-utils'
// =============================================================================
// 🔐 ЛОКАЛЬНЫЕ AUTH HELPERS
// =============================================================================
const checkFulfillmentAccess = async (userId: string) => {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { organization: true },
})
if (!user?.organizationId) {
throw new GraphQLError('Пользователь не привязан к организации', {
extensions: { code: 'FORBIDDEN' },
})
}
if (!user.organization || user.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент-центров', {
extensions: { code: 'FORBIDDEN' },
})
}
return user
}
// =============================================================================
// 📦 INVENTORY DOMAIN RESOLVERS
// =============================================================================
export const inventoryResolvers: DomainResolvers = {
Query: {
// Мои поставки расходников (для фулфилмента) - ОПТИМИЗИРОВАНО
myFulfillmentConsumableSupplies: withOrgTypeAuth(['FULFILLMENT'],
async (_: unknown, __: unknown, context: Context, user: any) => {
console.log('🔍 MY_FULFILLMENT_CONSUMABLE_SUPPLIES DOMAIN QUERY STARTED:', {
userId: context.user?.id,
organizationId: user.organizationId
})
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
fulfillmentCenterId: user.organizationId,
},
select: {
id: true,
status: true,
requestedDeliveryDate: true,
totalItems: true,
createdAt: true,
updatedAt: true,
// Оптимизированные select для связанных объектов
fulfillmentCenter: {
select: { id: true, name: true, type: true }
},
supplier: {
select: { id: true, name: true, type: true }
},
logisticsPartner: {
select: { id: true, name: true, type: true }
},
receivedBy: {
select: { id: true, managerName: true }
},
items: {
select: {
id: true,
quantity: true,
receivedQuantity: true,
product: {
select: { id: true, name: true, article: true, type: true }
}
}
},
},
orderBy: {
createdAt: 'desc',
},
take: 100, // Добавляем пагинацию для производительности
})
console.log('✅ MY_FULFILLMENT_CONSUMABLE_SUPPLIES DOMAIN SUCCESS:', { count: supplies.length })
return supplies
}
),
// Детальная информация о поставке расходников - ОПТИМИЗИРОВАНО
fulfillmentConsumableSupply: withAuth(async (_: unknown, args: { id: string }, context: Context) => {
console.log('🔍 FULFILLMENT_CONSUMABLE_SUPPLY DOMAIN QUERY STARTED:', {
userId: context.user?.id,
supplyId: args.id
})
// Используем кешированного пользователя
const user = await getCurrentUser(context)
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
select: {
id: true,
status: true,
requestedDeliveryDate: true,
totalItems: true,
fulfillmentCenterId: true,
supplierId: true,
createdAt: true,
updatedAt: true,
// Оптимизированные select
fulfillmentCenter: {
select: { id: true, name: true, type: true }
},
supplier: {
select: { id: true, name: true, type: true }
},
logisticsPartner: {
select: { id: true, name: true, type: true }
},
receivedBy: {
select: { id: true, managerName: true }
},
items: {
select: {
id: true,
quantity: true,
receivedQuantity: true,
product: {
select: { id: true, name: true, article: true, type: 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('Нет доступа к этой поставке')
}
console.log('✅ FULFILLMENT_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyId: supply.id })
return supply
}),
// Складские остатки фулфилмента (V2 система инвентаря)
myFulfillmentSupplies: withAuth(async (_: unknown, __: unknown, context: Context) => {
console.log('🔍 MY_FULFILLMENT_SUPPLIES DOMAIN QUERY STARTED:', { userId: context.user?.id })
try {
const user = await checkFulfillmentAccess(context.user!.id)
// Получаем складские остатки из новой V2 модели
const inventory = await prisma.fulfillmentConsumableInventory.findMany({
where: {
fulfillmentCenterId: user.organizationId!,
},
include: {
fulfillmentCenter: true,
product: {
include: {
organization: true, // Поставщик товара
},
},
},
orderBy: {
updatedAt: 'desc',
},
})
// Преобразуем V2 данные в формат Supply для совместимости с фронтендом
const suppliesFormatted = inventory.map((item) => {
const status = item.currentStock > 0 ? 'На складе' : 'Недоступен'
const supplier = item.product.organization?.name || 'Неизвестен'
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,
// ЦЕНЫ
price: parseFloat(item.averageCost.toString()),
pricePerUnit: item.resalePrice ? parseFloat(item.resalePrice.toString()) : null,
// СКЛАДСКИЕ ДАННЫЕ
currentStock: item.currentStock,
minStock: item.minStock,
usedStock: item.totalShipped || 0,
quantity: item.totalReceived,
warehouseStock: item.currentStock,
reservedStock: item.reservedStock,
// ОТГРУЗКИ
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.log('✅ MY_FULFILLMENT_SUPPLIES DOMAIN SUCCESS:', {
count: suppliesFormatted.length,
totalStock: suppliesFormatted.reduce((sum, item) => sum + item.currentStock, 0)
})
return suppliesFormatted
} catch (error) {
console.error('❌ MY_FULFILLMENT_SUPPLIES DOMAIN ERROR:', error)
return []
}
}),
// Расходники селлера на складе фулфилмента (для селлера)
mySellerConsumableInventory: withAuth(async (_: unknown, __: unknown, context: Context) => {
console.log('🔍 MY_SELLER_CONSUMABLE_INVENTORY DOMAIN QUERY STARTED:', { userId: context.user?.id })
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',
},
})
// Преобразуем V2 данные в формат Supply для совместимости с фронтендом
const suppliesFormatted = inventory.map((item) => {
const status = item.currentStock > 0 ? 'На складе' : 'Недоступен'
const supplier = item.product.organization?.name || 'Неизвестен'
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,
// ЦЕНЫ
price: parseFloat(item.averageCost.toString()),
pricePerUnit: item.usagePrice ? parseFloat(item.usagePrice.toString()) : null,
// СКЛАДСКИЕ ДАННЫЕ
currentStock: item.currentStock,
minStock: item.minStock,
usedStock: item.totalUsed || 0,
quantity: item.totalReceived,
warehouseStock: item.currentStock,
reservedStock: item.reservedStock,
// ИСПОЛЬЗОВАНИЕ
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.log('✅ MY_SELLER_CONSUMABLE_INVENTORY DOMAIN SUCCESS:', {
count: suppliesFormatted.length,
totalStock: suppliesFormatted.reduce((sum, item) => sum + item.currentStock, 0)
})
return suppliesFormatted
} catch (error) {
console.error('❌ MY_SELLER_CONSUMABLE_INVENTORY DOMAIN ERROR:', error)
return []
}
}),
// Расходники всех селлеров на складе фулфилмента (для фулфилмента)
allSellerConsumableInventory: withAuth(async (_: unknown, __: unknown, context: Context) => {
console.log('🔍 ALL_SELLER_CONSUMABLE_INVENTORY DOMAIN QUERY STARTED:', { userId: context.user?.id })
try {
const user = await checkFulfillmentAccess(context.user!.id)
// Получаем складские остатки всех селлеров на нашем складе
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' },
],
})
// Возвращаем данные сгруппированные по селлерам
const result = 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,
}
})
console.log('✅ ALL_SELLER_CONSUMABLE_INVENTORY DOMAIN SUCCESS:', {
count: result.length,
uniqueSellers: new Set(inventory.map(item => item.sellerId)).size
})
return result
} catch (error) {
console.error('❌ ALL_SELLER_CONSUMABLE_INVENTORY DOMAIN ERROR:', error)
return []
}
}),
// Заявки на поставки для поставщиков (новая система v2)
mySupplierConsumableSupplies: withAuth(async (_: unknown, __: unknown, context: Context) => {
console.log('🔍 MY_SUPPLIER_CONSUMABLE_SUPPLIES DOMAIN QUERY STARTED:', { userId: context.user?.id })
try {
const user = await prisma.user.findUnique({
where: { id: context.user!.id },
include: { organization: true },
})
if (!user?.organization || user.organization.type !== 'WHOLESALE') {
console.log('⚠️ User is not wholesale, returning empty array')
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',
},
})
console.log('✅ MY_SUPPLIER_CONSUMABLE_SUPPLIES DOMAIN SUCCESS:', { count: supplies.length })
return supplies
} catch (error) {
console.error('❌ MY_SUPPLIER_CONSUMABLE_SUPPLIES DOMAIN ERROR:', error)
return []
}
}),
// Расходники селлеров на складе фулфилмента (V2 система)
sellerSuppliesOnWarehouse: withAuth(async (_: unknown, __: unknown, context: Context) => {
console.log('🔍 SELLER_SUPPLIES_ON_WAREHOUSE DOMAIN QUERY STARTED:', { userId: context.user?.id })
try {
const user = await checkFulfillmentAccess(context.user!.id)
// V2: Получаем данные из SellerConsumableInventory
const sellerInventory = await prisma.sellerConsumableInventory.findMany({
where: {
fulfillmentCenterId: user.organizationId!,
},
include: {
seller: true,
fulfillmentCenter: true,
product: {
include: {
organization: true, // Поставщик товара
},
},
},
orderBy: [
{ seller: { name: 'asc' } }, // Группируем по селлерам
{ updatedAt: 'desc' },
],
})
console.log('📊 V2 Seller Inventory loaded for warehouse:', {
fulfillmentId: user.organizationId,
fulfillmentName: user.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 ? 'На складе' : 'Недоступен'
return {
// ИДЕНТИФИКАЦИЯ
id: item.id,
productId: item.product.id,
sellerId: item.seller.id,
// ОСНОВНЫЕ ДАННЫЕ
name: item.product.name,
article: item.product.article,
description: item.product.description || '',
unit: item.product.unit || 'шт',
imageUrl: item.product.imageUrl,
// ЦЕНЫ
price: parseFloat(item.averageCost.toString()),
// СКЛАДСКИЕ ДАННЫЕ
currentStock: item.currentStock,
quantity: item.totalReceived,
usedQuantity: item.totalShipped || 0,
// СТАТУС И МЕТАДАННЫЕ
status,
isAvailable: item.currentStock > 0,
sellerName: item.seller.name,
supplierName: item.product.organization?.name || 'Неизвестен',
date: item.lastSupplyDate?.toISOString() || item.createdAt.toISOString(),
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
}
})
console.log('✅ SELLER_SUPPLIES_ON_WAREHOUSE DOMAIN SUCCESS:', {
suppliesCount: suppliesFormatted.length,
sellersCount: new Set(sellerInventory.map(item => item.sellerId)).size,
})
return suppliesFormatted
} catch (error) {
console.error('❌ SELLER_SUPPLIES_ON_WAREHOUSE DOMAIN ERROR:', error)
return []
}
}),
// Данные склада с партнерами (3-уровневая иерархия)
warehouseData: withAuth(async (_: unknown, __: unknown, context: Context) => {
console.log('🔍 WAREHOUSE_DATA DOMAIN QUERY STARTED:', { userId: context.user?.id })
try {
const user = await checkFulfillmentAccess(context.user!.id)
// Получаем партнеров (селлеров) этого фулфилмента
const partnerships = await prisma.counterparty.findMany({
where: {
fulfillmentId: user.organizationId!,
status: 'APPROVED',
},
include: {
seller: {
include: {
users: true,
},
},
},
orderBy: { createdAt: 'desc' },
})
// Получаем данные склада по каждому партнеру
const warehouseData = await Promise.all(
partnerships.map(async (partnership) => {
// Получаем расходники этого селлера на складе
const inventory = await prisma.sellerConsumableInventory.findMany({
where: {
sellerId: partnership.sellerId,
fulfillmentCenterId: user.organizationId!,
},
include: {
product: {
include: {
organization: true,
},
},
},
})
// Подсчитываем статистику
const totalItems = inventory.reduce((sum, item) => sum + item.currentStock, 0)
const totalValue = inventory.reduce((sum, item) => sum + (item.currentStock * parseFloat(item.averageCost.toString())), 0)
const uniqueProducts = inventory.length
return {
// ПАРТНЕР (СЕЛЛЕР)
partnerId: partnership.seller.id,
partnerName: partnership.seller.name,
partnerInn: partnership.seller.inn,
partnerType: partnership.seller.type,
// СТАТИСТИКА СКЛАДА
totalItems,
totalValue,
uniqueProducts,
// ДЕТАЛИ ИНВЕНТАРЯ
inventory: inventory.map(item => ({
id: item.id,
productId: item.product.id,
name: item.product.name,
article: item.product.article,
currentStock: item.currentStock,
averageCost: parseFloat(item.averageCost.toString()),
supplierName: item.product.organization?.name || 'Неизвестен',
lastSupplyDate: item.lastSupplyDate,
})),
// МЕТАДАННЫЕ
partnershipDate: partnership.createdAt,
lastUpdate: partnership.updatedAt,
}
})
)
console.log('✅ WAREHOUSE_DATA DOMAIN SUCCESS:', {
partnersCount: warehouseData.length,
totalInventoryItems: warehouseData.reduce((sum, p) => sum + p.totalItems, 0),
})
return warehouseData
} catch (error) {
console.error('❌ WAREHOUSE_DATA DOMAIN ERROR:', error)
return []
}
}),
},
Mutation: {
// Создание поставки расходников (фулфилмент → поставщик)
createFulfillmentConsumableSupply: withAuth(async (
_: unknown,
args: {
input: {
supplierId: string
requestedDeliveryDate: string
items: Array<{
productId: string
requestedQuantity: number
}>
notes?: string
}
},
context: Context,
) => {
console.log('🔍 CREATE_FULFILLMENT_CONSUMABLE_SUPPLY DOMAIN MUTATION STARTED:', {
userId: context.user?.id,
supplierId: args.input.supplierId
})
try {
const user = await checkFulfillmentAccess(context.user!.id)
// Проверяем что поставщик существует и является 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,
},
})
const result = {
success: true,
message: 'Поставка расходников создана успешно',
supplyOrder,
}
console.log('✅ CREATE_FULFILLMENT_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyOrderId: supplyOrder.id })
return result
} catch (error: any) {
console.error('❌ CREATE_FULFILLMENT_CONSUMABLE_SUPPLY DOMAIN ERROR:', error)
return {
success: false,
message: error.message || 'Ошибка создания поставки',
supplyOrder: null,
}
}
}),
// Поставщик одобряет поставку расходников
supplierApproveConsumableSupply: withAuth(async (
_: unknown,
args: { id: string },
context: Context,
) => {
console.log('🔍 SUPPLIER_APPROVE_CONSUMABLE_SUPPLY DOMAIN MUTATION STARTED:', {
userId: context.user?.id,
supplyId: args.id
})
try {
const user = await checkWholesaleAccess(context.user!.id)
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,
},
},
},
})
const result = {
success: true,
message: 'Поставка одобрена успешно',
order: updatedSupply,
}
console.log('✅ SUPPLIER_APPROVE_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyId: updatedSupply.id })
return result
} catch (error: any) {
console.error('❌ SUPPLIER_APPROVE_CONSUMABLE_SUPPLY DOMAIN ERROR:', error)
return {
success: false,
message: error.message || 'Ошибка одобрения поставки',
order: null,
}
}
}),
// Поставщик отклоняет поставку расходников
supplierRejectConsumableSupply: withAuth(async (
_: unknown,
args: { id: string; reason?: string },
context: Context,
) => {
console.log('🔍 SUPPLIER_REJECT_CONSUMABLE_SUPPLY DOMAIN MUTATION STARTED:', {
userId: context.user?.id,
supplyId: args.id,
reason: args.reason
})
try {
const user = await checkWholesaleAccess(context.user!.id)
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,
},
},
},
})
const result = {
success: true,
message: 'Поставка отклонена',
order: updatedSupply,
}
console.log('✅ SUPPLIER_REJECT_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyId: updatedSupply.id })
return result
} catch (error: any) {
console.error('❌ SUPPLIER_REJECT_CONSUMABLE_SUPPLY DOMAIN ERROR:', error)
return {
success: false,
message: error.message || 'Ошибка отклонения поставки',
order: null,
}
}
}),
// Поставщик отправляет поставку расходников
supplierShipConsumableSupply: withAuth(async (
_: unknown,
args: { id: string },
context: Context,
) => {
console.log('🔍 SUPPLIER_SHIP_CONSUMABLE_SUPPLY DOMAIN MUTATION STARTED:', {
userId: context.user?.id,
supplyId: args.id
})
try {
const user = await checkWholesaleAccess(context.user!.id)
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,
},
},
},
})
const result = {
success: true,
message: 'Поставка отправлена',
order: updatedSupply,
}
console.log('✅ SUPPLIER_SHIP_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyId: updatedSupply.id })
return result
} catch (error: any) {
console.error('❌ SUPPLIER_SHIP_CONSUMABLE_SUPPLY DOMAIN ERROR:', error)
return {
success: false,
message: error.message || 'Ошибка отправки поставки',
order: null,
}
}
}),
// Фулфилмент принимает поставку расходников
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,
args: {
productId: string
quantity: number
reason?: string
},
context: Context,
) => {
console.warn('🔒 RESERVE_PRODUCT_STOCK (V1) - LEGACY RESOLVER:', {
productId: args.productId,
quantity: args.quantity,
reason: args.reason,
})
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
// TODO: Реализовать V1 логику резервирования товара на складе
// Может потребоваться миграция на V2 систему управления запасами
return {
success: false,
message: 'V1 Legacy - требуется реализация',
reservationId: null,
}
},
// V1 Legacy: Освобождение резерва товара
releaseProductReserve: async (
_: unknown,
args: {
reservationId: string
reason?: string
},
context: Context,
) => {
console.warn('🔓 RELEASE_PRODUCT_RESERVE (V1) - LEGACY RESOLVER:', {
reservationId: args.reservationId,
reason: args.reason,
})
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
// TODO: Реализовать V1 логику освобождения резерва товара
// Может потребоваться миграция на V2 систему управления резервированием
return {
success: false,
message: 'V1 Legacy - требуется реализация',
}
},
// V1 Legacy: Обновить статус товара в пути
updateProductInTransit: async (
_: unknown,
args: {
productId: string
transitData: {
status: string
location?: string
estimatedArrival?: string
trackingNumber?: string
}
},
context: Context,
) => {
console.warn('🚛 UPDATE_PRODUCT_IN_TRANSIT (V1) - LEGACY RESOLVER:', {
productId: args.productId,
transitStatus: args.transitData.status,
location: args.transitData.location,
})
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
// TODO: Реализовать V1 логику отслеживания товаров в пути
// Может потребоваться интеграция с V2 системой логистики
return {
success: false,
message: 'V1 Legacy - требуется реализация',
product: null,
}
},
},
}
console.warn('🔥 INVENTORY DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ')