Обновления системы после анализа и оптимизации архитектуры

- Обновлена схема Prisma с новыми полями и связями
- Актуализированы правила системы в rules-complete.md
- Оптимизированы GraphQL типы, запросы и мутации
- Улучшены компоненты интерфейса и валидация данных
- Исправлены критические ESLint ошибки: удалены неиспользуемые импорты и переменные
- Добавлены тестовые файлы для проверки функционала

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-06 23:44:49 +03:00
parent c2b342a527
commit 10af6f08cc
33 changed files with 3259 additions and 1319 deletions

View File

@ -687,28 +687,10 @@ export const resolvers = {
throw new GraphQLError('У пользователя нет организации')
}
// Получаем заказы поставок, где фулфилмент является получателем,
// но НЕ создателем (т.е. селлеры заказали расходники для фулфилмента)
const sellerSupplyOrders = await prisma.supplyOrder.findMany({
where: {
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
organizationId: { not: currentUser.organization.id }, // Создатель - НЕ мы
status: 'DELIVERED', // Только доставленные
},
include: {
organization: true,
partner: true,
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
},
})
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
return [] // Только фулфилменты имеют расходники
}
// Получаем ВСЕ расходники из таблицы supply для фулфилмента
const allSupplies = await prisma.supply.findMany({
@ -717,52 +699,39 @@ export const resolvers = {
orderBy: { createdAt: 'desc' },
})
// Получаем все заказы фулфилмента для себя (чтобы исключить их расходники)
const fulfillmentOwnOrders = await prisma.supplyOrder.findMany({
where: {
organizationId: currentUser.organization.id, // Созданы фулфилментом
fulfillmentCenterId: currentUser.organization.id, // Для себя
status: 'DELIVERED',
},
include: {
items: {
include: {
product: true,
},
},
},
})
// Преобразуем старую структуру в новую согласно GraphQL схеме
const transformedSupplies = allSupplies.map((supply) => ({
id: supply.id,
name: supply.name,
description: supply.description,
pricePerUnit: supply.price ? parseFloat(supply.price.toString()) : null, // Конвертируем Decimal в Number
unit: supply.unit || 'шт', // Единица измерения
imageUrl: supply.imageUrl,
warehouseStock: supply.currentStock || 0, // Остаток на складе
isAvailable: (supply.currentStock || 0) > 0, // Есть ли в наличии
warehouseConsumableId: supply.id, // Связь со складом (пока используем тот же ID)
createdAt: supply.createdAt,
updatedAt: supply.updatedAt,
organization: supply.organization,
}))
// Создаем набор названий товаров из заказов фулфилмента для себя
const fulfillmentProductNames = new Set(
fulfillmentOwnOrders.flatMap((order) => order.items.map((item) => item.product.name)),
)
// Фильтруем расходники: исключаем те, что созданы заказами фулфилмента для себя
const sellerSupplies = allSupplies.filter((supply) => {
// Если расходник соответствует товару из заказа фулфилмента для себя,
// то это расходник фулфилмента, а не селлера
return !fulfillmentProductNames.has(supply.name)
})
// Логирование для отладки
console.warn('🔥🔥🔥 SELLER SUPPLIES RESOLVER CALLED 🔥🔥🔥')
console.warn('📊 Расходники селлеров:', {
console.warn('🔥 SUPPLIES RESOLVER - NEW FORMAT:', {
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
allSuppliesCount: allSupplies.length,
fulfillmentOwnOrdersCount: fulfillmentOwnOrders.length,
fulfillmentProductNames: Array.from(fulfillmentProductNames),
filteredSellerSuppliesCount: sellerSupplies.length,
sellerOrdersCount: sellerSupplyOrders.length,
suppliesCount: transformedSupplies.length,
supplies: transformedSupplies.map(s => ({
id: s.id,
name: s.name,
pricePerUnit: s.pricePerUnit,
warehouseStock: s.warehouseStock,
isAvailable: s.isAvailable,
})),
})
// Возвращаем только расходники селлеров (исключая расходники фулфилмента)
return sellerSupplies
return transformedSupplies
},
// Расходники фулфилмента (материалы для работы фулфилмента)
myFulfillmentSupplies: async (_: unknown, __: unknown, context: Context) => {
// Доступные расходники для рецептур селлеров (только с ценой и в наличии)
getAvailableSuppliesForRecipe: async (_: unknown, __: unknown, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
@ -778,83 +747,90 @@ export const resolvers = {
throw new GraphQLError('У пользователя нет организации')
}
// TypeScript assertion - мы знаем что organization не null после проверки выше
const organization = currentUser.organization
// Селлеры могут получать расходники от своих фулфилмент-партнеров
if (currentUser.organization.type !== 'SELLER') {
return [] // Только селлеры используют рецептуры
}
// Получаем заказы поставок, созданные этим фулфилмент-центром для себя
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
// TODO: В будущем здесь будет логика получения расходников от партнерских фулфилментов
// Пока возвращаем пустой массив, так как эта функциональность еще разрабатывается
console.warn('🔥 getAvailableSuppliesForRecipe called for seller:', {
sellerId: currentUser.organization.id,
sellerName: currentUser.organization.name,
})
return []
},
// Расходники фулфилмента из склада (новая архитектура - синхронизация со склада)
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('Доступ только для фулфилмент центров')
}
// Получаем расходники фулфилмента из таблицы Supply
const supplies = await prisma.supply.findMany({
where: {
organizationId: organization.id, // Создали мы
fulfillmentCenterId: organization.id, // Получатель - мы
status: {
in: ['PENDING', 'CONFIRMED', 'IN_TRANSIT', 'DELIVERED'], // Все статусы
},
organizationId: currentUser.organization.id,
type: 'FULFILLMENT_CONSUMABLES', // Только расходники фулфилмента
},
include: {
partner: true,
items: {
include: {
product: {
include: {
category: true,
},
},
},
},
organization: true,
},
orderBy: { createdAt: 'desc' },
})
// Преобразуем заказы поставок в формат supply для единообразия
const fulfillmentSupplies = fulfillmentSupplyOrders.flatMap((order) =>
order.items.map((item) => ({
id: `fulfillment-order-${order.id}-${item.id}`,
name: item.product.name,
description: item.product.description || `Расходники от ${order.partner.name}`,
price: item.price,
quantity: item.quantity,
unit: 'шт',
category: item.product.category?.name || 'Расходники фулфилмента',
status:
order.status === 'PENDING'
? 'planned'
: order.status === 'CONFIRMED'
? 'confirmed'
: order.status === 'IN_TRANSIT'
? 'in-transit'
: order.status === 'DELIVERED'
? 'in-stock'
: 'planned',
date: order.createdAt,
supplier: order.partner.name || order.partner.fullName || 'Не указан',
minStock: Math.round(item.quantity * 0.1),
currentStock: order.status === 'DELIVERED' ? item.quantity : 0,
usedStock: 0, // TODO: Подсчитывать реальное использование
imageUrl: null,
createdAt: order.createdAt,
updatedAt: order.updatedAt,
organizationId: organization.id,
organization: organization,
shippedQuantity: 0,
})),
)
// Логирование для отладки
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED 🔥🔥🔥')
console.warn('📊 Расходники фулфилмента:', {
organizationId: organization.id,
organizationType: organization.type,
fulfillmentOrdersCount: fulfillmentSupplyOrders.length,
fulfillmentSuppliesCount: fulfillmentSupplies.length,
fulfillmentOrders: fulfillmentSupplyOrders.map((o) => ({
id: o.id,
supplierName: o.partner.name,
status: o.status,
itemsCount: o.items.length,
console.warn('🔥🔥🔥 FULFILLMENT SUPPLIES RESOLVER CALLED (NEW ARCHITECTURE) 🔥🔥🔥')
console.warn('📊 Расходники фулфилмента из склада:', {
organizationId: currentUser.organization.id,
organizationType: currentUser.organization.type,
suppliesCount: supplies.length,
supplies: supplies.map((s) => ({
id: s.id,
name: s.name,
type: s.type,
status: s.status,
currentStock: s.currentStock,
quantity: s.quantity,
})),
})
return fulfillmentSupplies
// Преобразуем в формат для фронтенда
return supplies.map(supply => ({
...supply,
price: supply.price ? parseFloat(supply.price.toString()) : 0,
shippedQuantity: 0, // Добавляем для совместимости
}))
},
// Заказы поставок расходников
@ -1411,11 +1387,6 @@ export const resolvers = {
// Мои товары и расходники (для поставщиков)
myProducts: async (_: unknown, __: unknown, context: Context) => {
console.warn('🔍 MY_PRODUCTS RESOLVER - ВЫЗВАН:', {
hasUser: !!context.user,
userId: context.user?.id,
timestamp: new Date().toISOString(),
})
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
@ -1428,12 +1399,6 @@ export const resolvers = {
include: { organization: true },
})
console.warn('👤 ПОЛЬЗОВАТЕЛЬ НАЙДЕН:', {
userId: currentUser?.id,
hasOrganization: !!currentUser?.organization,
organizationType: currentUser?.organization?.type,
organizationName: currentUser?.organization?.name,
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
@ -1441,10 +1406,6 @@ export const resolvers = {
// Проверяем, что это поставщик
if (currentUser.organization.type !== 'WHOLESALE') {
console.warn('❌ ДОСТУП ЗАПРЕЩЕН - НЕ ПОСТАВЩИК:', {
actualType: currentUser.organization.type,
requiredType: 'WHOLESALE',
})
throw new GraphQLError('Товары доступны только для поставщиков')
}
@ -2586,6 +2547,7 @@ export const resolvers = {
bik?: string
accountNumber?: string
corrAccount?: string
market?: string
}
},
context: Context,
@ -2636,6 +2598,7 @@ export const resolvers = {
emails?: object
managementName?: string
managementPost?: string
market?: string
} = {}
// Название организации больше не обновляется через профиль
@ -2651,6 +2614,11 @@ export const resolvers = {
updateData.emails = [{ value: input.email, type: 'main' }]
}
// Обновляем рынок для поставщиков
if (input.market !== undefined) {
updateData.market = input.market === 'none' ? null : input.market
}
// Сохраняем дополнительные контакты в custom полях
// Пока добавим их как дополнительные JSON поля
const customContacts: {
@ -3639,23 +3607,14 @@ export const resolvers = {
}
},
// Создать расходник
createSupply: async (
// Обновить цену расходника (новая архитектура - только цену можно редактировать)
updateSupplyPrice: async (
_: unknown,
args: {
id: string
input: {
name: string
description?: string
price: number
quantity: number
unit: string
category: string
status: string
date: string
supplier: string
minStock: number
currentStock: number
imageUrl?: string
pricePerUnit?: number | null
}
},
context: Context,
@ -3677,167 +3636,68 @@ export const resolvers = {
// Проверяем, что это фулфилмент центр
if (currentUser.organization.type !== 'FULFILLMENT') {
throw new GraphQLError('Расходники доступны только для фулфилмент центров')
throw new GraphQLError('Обновление цен расходников доступно только для фулфилмент центров')
}
try {
const supply = await prisma.supply.create({
data: {
name: args.input.name,
description: args.input.description,
price: args.input.price,
quantity: args.input.quantity,
unit: args.input.unit,
category: args.input.category,
status: args.input.status,
date: new Date(args.input.date),
supplier: args.input.supplier,
minStock: args.input.minStock,
currentStock: args.input.currentStock,
imageUrl: args.input.imageUrl,
// Находим и обновляем расходник
const existingSupply = await prisma.supply.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
include: { organization: true },
})
return {
success: true,
message: 'Расходник успешно создан',
supply,
if (!existingSupply) {
throw new GraphQLError('Расходник не найден')
}
} catch (error) {
console.error('Error creating supply:', error)
return {
success: false,
message: 'Ошибка при создании расходника',
}
}
},
// Обновить расходник
updateSupply: async (
_: unknown,
args: {
id: string
input: {
name: string
description?: string
price: number
quantity: number
unit: string
category: string
status: string
date: string
supplier: string
minStock: number
currentStock: number
imageUrl?: string
}
},
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что расходник принадлежит текущей организации
const existingSupply = await prisma.supply.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
})
if (!existingSupply) {
throw new GraphQLError('Расходник не найден или нет доступа')
}
try {
const supply = await prisma.supply.update({
const updatedSupply = await prisma.supply.update({
where: { id: args.id },
data: {
name: args.input.name,
description: args.input.description,
price: args.input.price,
quantity: args.input.quantity,
unit: args.input.unit,
category: args.input.category,
status: args.input.status,
date: new Date(args.input.date),
supplier: args.input.supplier,
minStock: args.input.minStock,
currentStock: args.input.currentStock,
imageUrl: args.input.imageUrl,
price: args.input.pricePerUnit, // Обновляем только цену
updatedAt: new Date(),
},
include: { organization: true },
})
// Преобразуем в новый формат для GraphQL
const transformedSupply = {
id: updatedSupply.id,
name: updatedSupply.name,
description: updatedSupply.description,
pricePerUnit: updatedSupply.price ? parseFloat(updatedSupply.price.toString()) : null, // Конвертируем Decimal в Number
unit: updatedSupply.unit || 'шт',
imageUrl: updatedSupply.imageUrl,
warehouseStock: updatedSupply.currentStock || 0,
isAvailable: (updatedSupply.currentStock || 0) > 0,
warehouseConsumableId: updatedSupply.id,
createdAt: updatedSupply.createdAt,
updatedAt: updatedSupply.updatedAt,
organization: updatedSupply.organization,
}
console.warn('🔥 SUPPLY PRICE UPDATED:', {
id: transformedSupply.id,
name: transformedSupply.name,
oldPrice: existingSupply.price,
newPrice: transformedSupply.pricePerUnit,
})
return {
success: true,
message: 'Расходник успешно обновлен',
supply,
message: 'Цена расходника успешно обновлена',
supply: transformedSupply,
}
} catch (error) {
console.error('Error updating supply:', error)
console.error('Error updating supply price:', error)
return {
success: false,
message: 'Ошибка при обновлении расходника',
message: 'Ошибка при обновлении цены расходника',
}
}
},
// Удалить расходник
deleteSupply: async (_: unknown, args: { id: string }, context: Context) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const currentUser = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!currentUser?.organization) {
throw new GraphQLError('У пользователя нет организации')
}
// Проверяем, что расходник принадлежит текущей организации
const existingSupply = await prisma.supply.findFirst({
where: {
id: args.id,
organizationId: currentUser.organization.id,
},
})
if (!existingSupply) {
throw new GraphQLError('Расходник не найден или нет доступа')
}
try {
await prisma.supply.delete({
where: { id: args.id },
})
return true
} catch (error) {
console.error('Error deleting supply:', error)
return false
}
},
// Использовать расходники фулфилмента
useFulfillmentSupplies: async (
_: unknown,