
- Добавлен feature flag USE_V2_GOODS_SYSTEM для переключения между V1 и V2 - Создан трансформер рецептур для конвертации V1 → V2 формата - Интегрирована V2 мутация CREATE_SELLER_GOODS_SUPPLY в useSupplyCart - Добавлен V2 запрос GET_MY_SELLER_GOODS_SUPPLIES в supplies-dashboard - Исправлены связи counterpartyOf в goods-supply-v2 resolver - Временно отключена валидация для не-MAIN_PRODUCT товаров в V2 - Создан новый компонент supplies-dashboard-v2 (в разработке) Изменения являются частью поэтапной миграции с V1 на V2 систему поставок 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
753 lines
24 KiB
TypeScript
753 lines
24 KiB
TypeScript
// =============================================================================
|
||
// 🛒 РЕЗОЛВЕРЫ ДЛЯ СИСТЕМЫ ПОСТАВОК ТОВАРОВ СЕЛЛЕРА 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('Ошибка отмены товарной поставки')
|
||
}
|
||
},
|
||
} |