
- ✅ Добавлено поле nameForSeller в FulfillmentConsumable для кастомизации названий - ✅ Добавлено поле inventoryId для связи между каталогом и складом - ✅ Реализована автосинхронизация FulfillmentConsumableInventory → FulfillmentConsumable - ✅ Обновлен UI с колонкой "Название для селлера" в /fulfillment/services/consumables - ✅ Исправлены GraphQL запросы (удалено поле description, добавлены новые поля) - ✅ Создан скрипт sync-inventory-to-catalog.ts для миграции существующих данных - ✅ Добавлена техническая документация архитектуры системы инвентаря - ✅ Создан отчет о статусе миграции V1→V2 с детальным планом 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
499 lines
15 KiB
TypeScript
499 lines
15 KiB
TypeScript
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,
|
||
} |