feat: реализовать V2 backend для товарных поставок селлера
- Создать модели: SellerGoodsSupplyOrder, SellerGoodsInventory, GoodsSupplyRecipeItem - Реализовать полные GraphQL resolvers с валидацией и авторизацией - Добавить автоматическое создание инвентаря при статусе DELIVERED - Внедрить нормализованную рецептуру с RecipeType enum - Подготовить функции для будущих отгрузок на маркетплейсы - Интегрировать V2 resolvers модульно в основную схему - Протестировать создание таблиц в БД 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,848 +1,745 @@
|
||||
// =============================================================================
|
||||
// 🛒 РЕЗОЛВЕРЫ ДЛЯ СИСТЕМЫ ПОСТАВОК ТОВАРОВ СЕЛЛЕРА 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'
|
||||
|
||||
// ========== GOODS SUPPLY V2 RESOLVERS (ЗАКОММЕНТИРОВАНО) ==========
|
||||
// Раскомментируйте для активации системы товарных поставок V2
|
||||
// =============================================================================
|
||||
// 🔍 QUERY RESOLVERS V2
|
||||
// =============================================================================
|
||||
|
||||
// ========== V2 RESOLVERS START ==========
|
||||
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 },
|
||||
})
|
||||
|
||||
export const goodsSupplyV2Resolvers = {
|
||||
Query: {
|
||||
// Товарные поставки селлера
|
||||
myGoodsSupplyOrdersV2: async (_: unknown, __: unknown, context: Context) => {
|
||||
const { user } = context
|
||||
|
||||
if (!user?.organization || user.organization.type !== 'SELLER') {
|
||||
throw new GraphQLError('Доступно только для селлеров', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const orders = await prisma.goodsSupplyOrder.findMany({
|
||||
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: {
|
||||
include: {
|
||||
phones: true,
|
||||
emails: true,
|
||||
},
|
||||
},
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
sizes: true,
|
||||
},
|
||||
},
|
||||
recipe: {
|
||||
include: {
|
||||
components: {
|
||||
include: {
|
||||
material: true,
|
||||
},
|
||||
},
|
||||
services: {
|
||||
include: {
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestedServices: {
|
||||
include: {
|
||||
service: true,
|
||||
completedBy: true,
|
||||
},
|
||||
},
|
||||
logisticsPartner: {
|
||||
include: {
|
||||
phones: true,
|
||||
},
|
||||
},
|
||||
supplier: {
|
||||
include: {
|
||||
phones: true,
|
||||
},
|
||||
},
|
||||
receivedBy: true,
|
||||
fulfillmentCenter: true,
|
||||
product: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
lastSupplyDate: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
return orders
|
||||
} catch (error) {
|
||||
throw new GraphQLError('Ошибка получения товарных поставок', {
|
||||
extensions: { code: 'INTERNAL_ERROR', originalError: error },
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Входящие товарные поставки (для фулфилмента)
|
||||
incomingGoodsSuppliesV2: async (_: unknown, __: unknown, context: Context) => {
|
||||
const { user } = context
|
||||
|
||||
if (!user?.organization || user.organization.type !== 'FULFILLMENT') {
|
||||
throw new GraphQLError('Доступно только для фулфилмент-центров', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const orders = await prisma.goodsSupplyOrder.findMany({
|
||||
} else if (user.organization.type === 'FULFILLMENT') {
|
||||
// Фулфилмент видит все товары на своем складе
|
||||
inventoryItems = await prisma.sellerGoodsInventory.findMany({
|
||||
where: {
|
||||
fulfillmentCenterId: user.organizationId!,
|
||||
},
|
||||
include: {
|
||||
seller: {
|
||||
include: {
|
||||
phones: true,
|
||||
emails: true,
|
||||
},
|
||||
},
|
||||
seller: true,
|
||||
fulfillmentCenter: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
recipe: {
|
||||
include: {
|
||||
components: {
|
||||
include: {
|
||||
material: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
unit: true,
|
||||
// НЕ показываем цены селлера
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
services: {
|
||||
include: {
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestedServices: {
|
||||
include: {
|
||||
service: true,
|
||||
completedBy: true,
|
||||
},
|
||||
},
|
||||
logisticsPartner: {
|
||||
include: {
|
||||
phones: true,
|
||||
},
|
||||
},
|
||||
supplier: true,
|
||||
receivedBy: true,
|
||||
product: true,
|
||||
},
|
||||
orderBy: {
|
||||
requestedDeliveryDate: 'asc',
|
||||
lastSupplyDate: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
// Фильтруем коммерческие данные селлера
|
||||
return orders.map(order => ({
|
||||
...order,
|
||||
items: order.items.map(item => ({
|
||||
...item,
|
||||
price: null, // Скрываем закупочную цену селлера
|
||||
totalPrice: null, // Скрываем общую стоимость
|
||||
})),
|
||||
}))
|
||||
} catch (error) {
|
||||
throw new GraphQLError('Ошибка получения входящих поставок', {
|
||||
extensions: { code: 'INTERNAL_ERROR', originalError: error },
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Товарные заказы для поставщиков
|
||||
myGoodsSupplyRequestsV2: async (_: unknown, __: unknown, context: Context) => {
|
||||
const { user } = context
|
||||
|
||||
if (!user?.organization || user.organization.type !== 'WHOLESALE') {
|
||||
throw new GraphQLError('Доступно только для поставщиков', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const orders = await prisma.goodsSupplyOrder.findMany({
|
||||
where: {
|
||||
supplierId: user.organizationId!,
|
||||
},
|
||||
include: {
|
||||
seller: {
|
||||
include: {
|
||||
phones: true,
|
||||
},
|
||||
},
|
||||
fulfillmentCenter: true,
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// НЕ включаем requestedServices - поставщик не видит услуги ФФ
|
||||
},
|
||||
orderBy: {
|
||||
requestedDeliveryDate: 'asc',
|
||||
},
|
||||
})
|
||||
return inventoryItems
|
||||
} catch (error) {
|
||||
console.error('Error fetching seller goods inventory:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Показываем только релевантную для поставщика информацию
|
||||
return orders.map(order => ({
|
||||
...order,
|
||||
items: order.items.map(item => ({
|
||||
...item,
|
||||
recipe: null, // Поставщик не видит рецептуры
|
||||
})),
|
||||
}))
|
||||
} catch (error) {
|
||||
throw new GraphQLError('Ошибка получения заказов поставок', {
|
||||
extensions: { code: 'INTERNAL_ERROR', originalError: error },
|
||||
})
|
||||
}
|
||||
},
|
||||
// =============================================================================
|
||||
// ✏️ MUTATION RESOLVERS V2
|
||||
// =============================================================================
|
||||
|
||||
// Детали конкретной поставки
|
||||
goodsSupplyOrderV2: async (_: unknown, args: { id: string }, context: Context) => {
|
||||
const { user } = context
|
||||
|
||||
if (!user?.organizationId) {
|
||||
throw new GraphQLError('Необходима авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
export const sellerGoodsMutations = {
|
||||
// Создание поставки товаров селлера
|
||||
createSellerGoodsSupply: async (_: unknown, args: { input: any }, context: Context) => {
|
||||
if (!context.user) {
|
||||
throw new GraphQLError('Требуется авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const order = await prisma.goodsSupplyOrder.findUnique({
|
||||
where: { id: args.id },
|
||||
include: {
|
||||
seller: {
|
||||
include: {
|
||||
phones: true,
|
||||
emails: true,
|
||||
},
|
||||
},
|
||||
fulfillmentCenter: {
|
||||
include: {
|
||||
phones: true,
|
||||
emails: true,
|
||||
},
|
||||
},
|
||||
items: {
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
sizes: true,
|
||||
},
|
||||
},
|
||||
recipe: {
|
||||
include: {
|
||||
components: {
|
||||
include: {
|
||||
material: true,
|
||||
},
|
||||
},
|
||||
services: {
|
||||
include: {
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestedServices: {
|
||||
include: {
|
||||
service: true,
|
||||
completedBy: true,
|
||||
},
|
||||
},
|
||||
logisticsPartner: {
|
||||
include: {
|
||||
phones: true,
|
||||
emails: true,
|
||||
},
|
||||
},
|
||||
supplier: {
|
||||
include: {
|
||||
phones: true,
|
||||
emails: true,
|
||||
},
|
||||
},
|
||||
receivedBy: true,
|
||||
},
|
||||
})
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: context.user.id },
|
||||
include: { organization: true },
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
throw new GraphQLError('Поставка не найдена', {
|
||||
extensions: { code: 'NOT_FOUND' },
|
||||
})
|
||||
}
|
||||
|
||||
// Проверка прав доступа
|
||||
const hasAccess =
|
||||
order.sellerId === user.organizationId ||
|
||||
order.fulfillmentCenterId === user.organizationId ||
|
||||
order.supplierId === user.organizationId ||
|
||||
order.logisticsPartnerId === user.organizationId
|
||||
|
||||
if (!hasAccess) {
|
||||
throw new GraphQLError('Доступ запрещен', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
|
||||
// Фильтрация данных в зависимости от роли
|
||||
if (user.organization?.type === 'WHOLESALE') {
|
||||
// Поставщик не видит рецептуры и услуги ФФ
|
||||
return {
|
||||
...order,
|
||||
items: order.items.map(item => ({
|
||||
...item,
|
||||
recipe: null,
|
||||
})),
|
||||
requestedServices: [],
|
||||
}
|
||||
}
|
||||
|
||||
if (user.organization?.type === 'FULFILLMENT') {
|
||||
// ФФ не видит закупочные цены селлера
|
||||
return {
|
||||
...order,
|
||||
items: order.items.map(item => ({
|
||||
...item,
|
||||
price: null,
|
||||
totalPrice: null,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
if (user.organization?.type === 'LOGIST') {
|
||||
// Логистика видит только логистическую информацию
|
||||
return {
|
||||
...order,
|
||||
items: order.items.map(item => ({
|
||||
...item,
|
||||
price: null,
|
||||
totalPrice: null,
|
||||
recipe: null,
|
||||
})),
|
||||
requestedServices: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Селлер видит все свои данные
|
||||
return order
|
||||
} catch (error) {
|
||||
if (error instanceof GraphQLError) {
|
||||
throw error
|
||||
}
|
||||
throw new GraphQLError('Ошибка получения поставки', {
|
||||
extensions: { code: 'INTERNAL_ERROR', originalError: error },
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Рецептуры товаров селлера
|
||||
myProductRecipes: async (_: unknown, __: unknown, context: Context) => {
|
||||
const { user } = context
|
||||
|
||||
if (!user?.organization || user.organization.type !== 'SELLER') {
|
||||
throw new GraphQLError('Доступно только для селлеров', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
throw new GraphQLError('Доступно только для селлеров')
|
||||
}
|
||||
|
||||
try {
|
||||
const recipes = await prisma.productRecipe.findMany({
|
||||
where: {
|
||||
product: {
|
||||
organizationId: user.organizationId!,
|
||||
},
|
||||
const { fulfillmentCenterId, supplierId, logisticsPartnerId, requestedDeliveryDate, notes, recipeItems } = args.input
|
||||
|
||||
// 🔍 ВАЛИДАЦИЯ ПАРТНЕРОВ
|
||||
|
||||
// Проверяем фулфилмент-центр
|
||||
const fulfillmentCenter = await prisma.organization.findUnique({
|
||||
where: { id: fulfillmentCenterId },
|
||||
include: {
|
||||
counterpartiesAsCounterparty: {
|
||||
where: { organizationId: user.organizationId! },
|
||||
},
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
include: {
|
||||
material: true,
|
||||
},
|
||||
},
|
||||
services: {
|
||||
include: {
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!fulfillmentCenter || fulfillmentCenter.type !== 'FULFILLMENT') {
|
||||
throw new GraphQLError('Фулфилмент-центр не найден или имеет неверный тип')
|
||||
}
|
||||
|
||||
if (fulfillmentCenter.counterpartiesAsCounterparty.length === 0) {
|
||||
throw new GraphQLError('Нет партнерских отношений с данным фулфилмент-центром')
|
||||
}
|
||||
|
||||
// Проверяем поставщика
|
||||
const supplier = await prisma.organization.findUnique({
|
||||
where: { id: supplierId },
|
||||
include: {
|
||||
counterpartiesAsCounterparty: {
|
||||
where: { organizationId: user.organizationId! },
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
if (!supplier || supplier.type !== 'WHOLESALE') {
|
||||
throw new GraphQLError('Поставщик не найден или имеет неверный тип')
|
||||
}
|
||||
|
||||
if (supplier.counterpartiesAsCounterparty.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('Должен быть хотя бы один основной товар')
|
||||
}
|
||||
|
||||
// Проверяем все товары в рецептуре
|
||||
for (const item of recipeItems) {
|
||||
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 (item.recipeType === 'MAIN_PRODUCT') {
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
return recipes
|
||||
} catch (error) {
|
||||
throw new GraphQLError('Ошибка получения рецептур', {
|
||||
extensions: { code: 'INTERNAL_ERROR', originalError: error },
|
||||
})
|
||||
// Создаем записи рецептуры
|
||||
for (const item of recipeItems) {
|
||||
await tx.goodsSupplyRecipeItem.create({
|
||||
data: {
|
||||
supplyOrderId: newOrder.id,
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
recipeType: item.recipeType,
|
||||
},
|
||||
})
|
||||
|
||||
// Резервируем основные товары у поставщика
|
||||
if (item.recipeType === 'MAIN_PRODUCT') {
|
||||
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('Ошибка создания товарной поставки')
|
||||
}
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
// Создание товарной поставки
|
||||
createGoodsSupplyOrder: async (_: unknown, args: any, context: Context) => {
|
||||
const { user } = context
|
||||
const { input } = args
|
||||
|
||||
if (!user?.organization || user.organization.type !== 'SELLER') {
|
||||
throw new GraphQLError('Доступно только для селлеров', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
// Обновление статуса товарной поставки
|
||||
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('Организация не найдена')
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем фулфилмент-центр
|
||||
const fulfillmentCenter = await prisma.organization.findFirst({
|
||||
where: {
|
||||
id: input.fulfillmentCenterId,
|
||||
type: 'FULFILLMENT',
|
||||
const supply = await prisma.sellerGoodsSupplyOrder.findUnique({
|
||||
where: { id: args.id },
|
||||
include: {
|
||||
seller: true,
|
||||
supplier: true,
|
||||
fulfillmentCenter: true,
|
||||
recipeItems: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
if (!fulfillmentCenter) {
|
||||
throw new GraphQLError('Фулфилмент-центр не найден', {
|
||||
extensions: { code: 'NOT_FOUND' },
|
||||
})
|
||||
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('Только поставщик может одобрить заказ')
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем товары и рецептуры
|
||||
for (const item of input.items) {
|
||||
const product = await prisma.product.findFirst({
|
||||
where: {
|
||||
id: item.productId,
|
||||
organizationId: user.organizationId!,
|
||||
},
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
throw new GraphQLError(`Товар ${item.productId} не найден`, {
|
||||
extensions: { code: 'NOT_FOUND' },
|
||||
})
|
||||
}
|
||||
|
||||
if (item.recipeId) {
|
||||
const recipe = await prisma.productRecipe.findFirst({
|
||||
where: {
|
||||
id: item.recipeId,
|
||||
productId: item.productId,
|
||||
},
|
||||
})
|
||||
|
||||
if (!recipe) {
|
||||
throw new GraphQLError(`Рецептура ${item.recipeId} не найдена`, {
|
||||
extensions: { code: 'NOT_FOUND' },
|
||||
})
|
||||
}
|
||||
}
|
||||
// Только поставщики могут переводить APPROVED → SHIPPED
|
||||
else if (status === 'SHIPPED' && currentStatus === 'APPROVED') {
|
||||
if (orgType !== 'WHOLESALE' || supply.supplierId !== user.organizationId) {
|
||||
throw new GraphQLError('Только поставщик может отметить отгрузку')
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем поставку в транзакции
|
||||
const order = await prisma.$transaction(async (tx) => {
|
||||
// Создаем основную запись
|
||||
const newOrder = await tx.goodsSupplyOrder.create({
|
||||
data: {
|
||||
sellerId: user.organizationId!,
|
||||
fulfillmentCenterId: input.fulfillmentCenterId,
|
||||
requestedDeliveryDate: new Date(input.requestedDeliveryDate),
|
||||
notes: input.notes,
|
||||
status: 'PENDING',
|
||||
// Только фулфилмент может переводить 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,
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Создаем товары
|
||||
let totalAmount = 0
|
||||
let totalItems = 0
|
||||
// 📨 УВЕДОМЛЕНИЯ О СМЕНЕ СТАТУСА
|
||||
if (status === 'APPROVED') {
|
||||
await notifyOrganization(
|
||||
supply.sellerId,
|
||||
`Поставка товаров одобрена поставщиком ${user.organization.name}`,
|
||||
'GOODS_SUPPLY_APPROVED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
}
|
||||
|
||||
for (const itemInput of input.items) {
|
||||
const itemTotal = itemInput.price * itemInput.quantity
|
||||
totalAmount += itemTotal
|
||||
totalItems += itemInput.quantity
|
||||
if (status === 'SHIPPED') {
|
||||
await notifyOrganization(
|
||||
supply.sellerId,
|
||||
`Поставка товаров отгружена поставщиком ${user.organization.name}`,
|
||||
'GOODS_SUPPLY_SHIPPED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
|
||||
await tx.goodsSupplyOrderItem.create({
|
||||
data: {
|
||||
orderId: newOrder.id,
|
||||
productId: itemInput.productId,
|
||||
quantity: itemInput.quantity,
|
||||
price: itemInput.price,
|
||||
totalPrice: itemTotal,
|
||||
recipeId: itemInput.recipeId,
|
||||
},
|
||||
})
|
||||
}
|
||||
await notifyOrganization(
|
||||
supply.fulfillmentCenterId,
|
||||
'Поставка товаров в пути. Ожидается доставка',
|
||||
'GOODS_SUPPLY_IN_TRANSIT',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
}
|
||||
|
||||
// Создаем запросы услуг
|
||||
for (const serviceInput of input.requestedServices) {
|
||||
const service = await tx.service.findUnique({
|
||||
where: { id: serviceInput.serviceId },
|
||||
})
|
||||
if (status === 'DELIVERED') {
|
||||
// 📦 АВТОМАТИЧЕСКОЕ СОЗДАНИЕ/ОБНОВЛЕНИЕ ИНВЕНТАРЯ V2
|
||||
await processSellerGoodsSupplyReceipt(args.id)
|
||||
|
||||
await notifyOrganization(
|
||||
supply.sellerId,
|
||||
`Поставка товаров доставлена в ${supply.fulfillmentCenter.name}`,
|
||||
'GOODS_SUPPLY_DELIVERED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
}
|
||||
|
||||
if (!service) {
|
||||
throw new Error(`Услуга ${serviceInput.serviceId} не найдена`)
|
||||
}
|
||||
if (status === 'COMPLETED') {
|
||||
await notifyOrganization(
|
||||
supply.sellerId,
|
||||
`Поставка товаров завершена. Товары размещены на складе ${supply.fulfillmentCenter.name}`,
|
||||
'GOODS_SUPPLY_COMPLETED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
}
|
||||
|
||||
const serviceTotal = service.price * serviceInput.quantity
|
||||
totalAmount += serviceTotal
|
||||
return updatedSupply
|
||||
} catch (error) {
|
||||
console.error('Error updating seller goods supply status:', error)
|
||||
|
||||
await tx.fulfillmentServiceRequest.create({
|
||||
data: {
|
||||
orderId: newOrder.id,
|
||||
serviceId: serviceInput.serviceId,
|
||||
quantity: serviceInput.quantity,
|
||||
price: service.price,
|
||||
totalPrice: serviceTotal,
|
||||
status: 'PENDING',
|
||||
},
|
||||
})
|
||||
}
|
||||
if (error instanceof GraphQLError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Обновляем итоги
|
||||
await tx.goodsSupplyOrder.update({
|
||||
where: { id: newOrder.id },
|
||||
data: {
|
||||
totalAmount,
|
||||
totalItems,
|
||||
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,
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return newOrder
|
||||
})
|
||||
if (!supply) {
|
||||
throw new GraphQLError('Поставка не найдена')
|
||||
}
|
||||
|
||||
// Получаем созданную поставку с полными данными
|
||||
const createdOrder = await prisma.goodsSupplyOrder.findUnique({
|
||||
where: { id: order.id },
|
||||
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,
|
||||
items: {
|
||||
supplier: true,
|
||||
recipeItems: {
|
||||
include: {
|
||||
product: true,
|
||||
recipe: true,
|
||||
},
|
||||
},
|
||||
requestedServices: {
|
||||
include: {
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Товарная поставка успешно создана',
|
||||
order: createdOrder,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof GraphQLError) {
|
||||
throw error
|
||||
}
|
||||
throw new GraphQLError('Ошибка создания поставки', {
|
||||
extensions: { code: 'INTERNAL_ERROR', originalError: error },
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Обновление статуса товарной поставки
|
||||
updateGoodsSupplyOrderStatus: async (_: unknown, args: any, context: Context) => {
|
||||
const { user } = context
|
||||
const { id, status, notes } = args
|
||||
|
||||
if (!user?.organizationId) {
|
||||
throw new GraphQLError('Необходима авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const order = await prisma.goodsSupplyOrder.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
throw new GraphQLError('Поставка не найдена', {
|
||||
extensions: { code: 'NOT_FOUND' },
|
||||
})
|
||||
}
|
||||
|
||||
// Проверка прав на изменение статуса
|
||||
const canUpdate =
|
||||
(status === 'SUPPLIER_APPROVED' && order.supplierId === user.organizationId) ||
|
||||
(status === 'LOGISTICS_CONFIRMED' && user.organization?.type === 'FULFILLMENT') ||
|
||||
(status === 'SHIPPED' && order.supplierId === user.organizationId) ||
|
||||
(status === 'IN_TRANSIT' && order.logisticsPartnerId === user.organizationId) ||
|
||||
(status === 'RECEIVED' && order.fulfillmentCenterId === user.organizationId) ||
|
||||
(status === 'CANCELLED' &&
|
||||
(order.sellerId === user.organizationId || order.fulfillmentCenterId === user.organizationId))
|
||||
|
||||
if (!canUpdate) {
|
||||
throw new GraphQLError('Недостаточно прав для изменения статуса', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
|
||||
const updateData: any = {
|
||||
status,
|
||||
notes: notes || order.notes,
|
||||
}
|
||||
|
||||
// Устанавливаем временные метки
|
||||
if (status === 'SUPPLIER_APPROVED') {
|
||||
updateData.supplierApprovedAt = new Date()
|
||||
} else if (status === 'SHIPPED') {
|
||||
updateData.shippedAt = new Date()
|
||||
} else if (status === 'RECEIVED') {
|
||||
updateData.receivedAt = new Date()
|
||||
updateData.receivedById = user.id
|
||||
}
|
||||
|
||||
const updatedOrder = await prisma.goodsSupplyOrder.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
receivedBy: true,
|
||||
},
|
||||
})
|
||||
|
||||
return updatedOrder
|
||||
} catch (error) {
|
||||
if (error instanceof GraphQLError) {
|
||||
throw error
|
||||
}
|
||||
throw new GraphQLError('Ошибка обновления статуса', {
|
||||
extensions: { code: 'INTERNAL_ERROR', originalError: error },
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Приемка товарной поставки
|
||||
receiveGoodsSupplyOrder: async (_: unknown, args: any, context: Context) => {
|
||||
const { user } = context
|
||||
const { id, items } = args
|
||||
|
||||
if (!user?.organization || user.organization.type !== 'FULFILLMENT') {
|
||||
throw new GraphQLError('Доступно только для фулфилмент-центров', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const order = await prisma.goodsSupplyOrder.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
recipe: {
|
||||
include: {
|
||||
components: {
|
||||
include: {
|
||||
material: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
throw new GraphQLError('Поставка не найдена', {
|
||||
extensions: { code: 'NOT_FOUND' },
|
||||
})
|
||||
}
|
||||
|
||||
if (order.fulfillmentCenterId !== user.organizationId) {
|
||||
throw new GraphQLError('Доступ запрещен', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
|
||||
if (order.status !== 'IN_TRANSIT') {
|
||||
throw new GraphQLError('Поставка должна быть в статусе "В пути"', {
|
||||
extensions: { code: 'BAD_REQUEST' },
|
||||
})
|
||||
}
|
||||
|
||||
// Обрабатываем приемку в транзакции
|
||||
const updatedOrder = await prisma.$transaction(async (tx) => {
|
||||
// Обновляем данные приемки для каждого товара
|
||||
for (const itemInput of items) {
|
||||
const orderItem = order.items.find(item => item.id === itemInput.itemId)
|
||||
if (!orderItem) {
|
||||
throw new Error(`Товар ${itemInput.itemId} не найден в поставке`)
|
||||
}
|
||||
|
||||
await tx.goodsSupplyOrderItem.update({
|
||||
where: { id: itemInput.itemId },
|
||||
// Освобождаем зарезервированные товары у поставщика (только MAIN_PRODUCT)
|
||||
for (const item of supply.recipeItems) {
|
||||
if (item.recipeType === 'MAIN_PRODUCT') {
|
||||
await tx.product.update({
|
||||
where: { id: item.productId },
|
||||
data: {
|
||||
receivedQuantity: itemInput.receivedQuantity,
|
||||
damagedQuantity: itemInput.damagedQuantity || 0,
|
||||
acceptanceNotes: itemInput.acceptanceNotes,
|
||||
ordered: {
|
||||
decrement: item.quantity,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Обновляем остатки расходников по рецептуре
|
||||
if (orderItem.recipe && itemInput.receivedQuantity > 0) {
|
||||
for (const component of orderItem.recipe.components) {
|
||||
const usedQuantity = component.quantity * itemInput.receivedQuantity
|
||||
|
||||
await tx.supply.update({
|
||||
where: { id: component.materialId },
|
||||
data: {
|
||||
currentStock: {
|
||||
decrement: usedQuantity,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем статус поставки
|
||||
const updated = await tx.goodsSupplyOrder.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'RECEIVED',
|
||||
receivedAt: new Date(),
|
||||
receivedById: user.id,
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
recipe: {
|
||||
include: {
|
||||
components: {
|
||||
include: {
|
||||
material: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestedServices: {
|
||||
include: {
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
receivedBy: true,
|
||||
},
|
||||
})
|
||||
|
||||
return updated
|
||||
})
|
||||
|
||||
return updatedOrder
|
||||
} catch (error) {
|
||||
if (error instanceof GraphQLError) {
|
||||
throw error
|
||||
}
|
||||
throw new GraphQLError('Ошибка приемки поставки', {
|
||||
extensions: { code: 'INTERNAL_ERROR', originalError: error },
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Отмена товарной поставки
|
||||
cancelGoodsSupplyOrder: async (_: unknown, args: any, context: Context) => {
|
||||
const { user } = context
|
||||
const { id, reason } = args
|
||||
|
||||
if (!user?.organizationId) {
|
||||
throw new GraphQLError('Необходима авторизация', {
|
||||
extensions: { code: 'UNAUTHENTICATED' },
|
||||
})
|
||||
return updated
|
||||
})
|
||||
|
||||
// 📨 УВЕДОМЛЕНИЯ ОБ ОТМЕНЕ
|
||||
if (supply.supplierId) {
|
||||
await notifyOrganization(
|
||||
supply.supplierId,
|
||||
`Селлер ${user.organization.name} отменил заказ товаров`,
|
||||
'GOODS_SUPPLY_CANCELLED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const order = await prisma.goodsSupplyOrder.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
await notifyOrganization(
|
||||
supply.fulfillmentCenterId,
|
||||
`Селлер ${user.organization.name} отменил поставку товаров`,
|
||||
'GOODS_SUPPLY_CANCELLED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
|
||||
if (!order) {
|
||||
throw new GraphQLError('Поставка не найдена', {
|
||||
extensions: { code: 'NOT_FOUND' },
|
||||
})
|
||||
}
|
||||
return cancelledSupply
|
||||
} catch (error) {
|
||||
console.error('Error cancelling seller goods supply:', error)
|
||||
|
||||
// Проверка прав на отмену
|
||||
const canCancel =
|
||||
order.sellerId === user.organizationId ||
|
||||
order.fulfillmentCenterId === user.organizationId ||
|
||||
(order.supplierId === user.organizationId && order.status === 'PENDING')
|
||||
|
||||
if (!canCancel) {
|
||||
throw new GraphQLError('Недостаточно прав для отмены поставки', {
|
||||
extensions: { code: 'FORBIDDEN' },
|
||||
})
|
||||
}
|
||||
|
||||
if (['RECEIVED', 'PROCESSING', 'COMPLETED'].includes(order.status)) {
|
||||
throw new GraphQLError('Нельзя отменить поставку в текущем статусе', {
|
||||
extensions: { code: 'BAD_REQUEST' },
|
||||
})
|
||||
}
|
||||
|
||||
const cancelledOrder = await prisma.goodsSupplyOrder.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'CANCELLED',
|
||||
notes: `${order.notes ? order.notes + '\n' : ''}ОТМЕНЕНО: ${reason}`,
|
||||
},
|
||||
})
|
||||
|
||||
return cancelledOrder
|
||||
} catch (error) {
|
||||
if (error instanceof GraphQLError) {
|
||||
throw error
|
||||
}
|
||||
throw new GraphQLError('Ошибка отмены поставки', {
|
||||
extensions: { code: 'INTERNAL_ERROR', originalError: error },
|
||||
})
|
||||
if (error instanceof GraphQLError) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
throw new GraphQLError('Ошибка отмены товарной поставки')
|
||||
}
|
||||
},
|
||||
}
|
Reference in New Issue
Block a user