feat(graphql): добавить V2 GraphQL queries и resolvers для поставок расходников
Добавлены: - seller-consumables-v2.ts - GraphQL queries для селлеров V2 системы - seller-consumables.ts - resolver для работы с поставками расходников селлеров 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
320
src/graphql/queries/seller-consumables-v2.ts
Normal file
320
src/graphql/queries/seller-consumables-v2.ts
Normal file
@ -0,0 +1,320 @@
|
||||
// =============================================================================
|
||||
// 📦 GraphQL ЗАПРОСЫ ДЛЯ СИСТЕМЫ ПОСТАВОК РАСХОДНИКОВ СЕЛЛЕРА V2
|
||||
// =============================================================================
|
||||
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
// =============================================================================
|
||||
// 🔍 QUERY - ПОЛУЧЕНИЕ ДАННЫХ
|
||||
// =============================================================================
|
||||
|
||||
export const GET_MY_SELLER_CONSUMABLE_SUPPLIES = gql`
|
||||
query GetMySellerConsumableSupplies {
|
||||
mySellerConsumableSupplies {
|
||||
id
|
||||
status
|
||||
sellerId
|
||||
seller {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
fulfillmentCenterId
|
||||
fulfillmentCenter {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
requestedDeliveryDate
|
||||
notes
|
||||
|
||||
# Данные поставщика
|
||||
supplierId
|
||||
supplier {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
supplierApprovedAt
|
||||
packagesCount
|
||||
estimatedVolume
|
||||
supplierContractId
|
||||
supplierNotes
|
||||
|
||||
# Данные логистики
|
||||
logisticsPartnerId
|
||||
logisticsPartner {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
estimatedDeliveryDate
|
||||
routeId
|
||||
logisticsCost
|
||||
logisticsNotes
|
||||
|
||||
# Данные отгрузки
|
||||
shippedAt
|
||||
trackingNumber
|
||||
|
||||
# Данные приемки
|
||||
deliveredAt
|
||||
receivedById
|
||||
receivedBy {
|
||||
id
|
||||
managerName
|
||||
phone
|
||||
}
|
||||
actualQuantity
|
||||
defectQuantity
|
||||
receiptNotes
|
||||
|
||||
# Экономика
|
||||
totalCostWithDelivery
|
||||
estimatedStorageCost
|
||||
|
||||
items {
|
||||
id
|
||||
productId
|
||||
product {
|
||||
id
|
||||
name
|
||||
article
|
||||
price
|
||||
quantity
|
||||
mainImage
|
||||
}
|
||||
requestedQuantity
|
||||
approvedQuantity
|
||||
shippedQuantity
|
||||
receivedQuantity
|
||||
defectQuantity
|
||||
unitPrice
|
||||
totalPrice
|
||||
}
|
||||
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_SELLER_CONSUMABLE_SUPPLY = gql`
|
||||
query GetSellerConsumableSupply($id: ID!) {
|
||||
sellerConsumableSupply(id: $id) {
|
||||
id
|
||||
status
|
||||
sellerId
|
||||
seller {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
fulfillmentCenterId
|
||||
fulfillmentCenter {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
requestedDeliveryDate
|
||||
notes
|
||||
|
||||
# Данные поставщика
|
||||
supplierId
|
||||
supplier {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
supplierApprovedAt
|
||||
packagesCount
|
||||
estimatedVolume
|
||||
supplierContractId
|
||||
supplierNotes
|
||||
|
||||
# Данные логистики
|
||||
logisticsPartnerId
|
||||
logisticsPartner {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
estimatedDeliveryDate
|
||||
routeId
|
||||
logisticsCost
|
||||
logisticsNotes
|
||||
|
||||
# Данные отгрузки
|
||||
shippedAt
|
||||
trackingNumber
|
||||
|
||||
# Данные приемки
|
||||
deliveredAt
|
||||
receivedById
|
||||
receivedBy {
|
||||
id
|
||||
managerName
|
||||
phone
|
||||
}
|
||||
actualQuantity
|
||||
defectQuantity
|
||||
receiptNotes
|
||||
|
||||
# Экономика
|
||||
totalCostWithDelivery
|
||||
estimatedStorageCost
|
||||
|
||||
items {
|
||||
id
|
||||
productId
|
||||
product {
|
||||
id
|
||||
name
|
||||
article
|
||||
price
|
||||
quantity
|
||||
mainImage
|
||||
}
|
||||
requestedQuantity
|
||||
approvedQuantity
|
||||
shippedQuantity
|
||||
receivedQuantity
|
||||
defectQuantity
|
||||
unitPrice
|
||||
totalPrice
|
||||
}
|
||||
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Для других типов организаций (фулфилмент, поставщики)
|
||||
export const GET_INCOMING_SELLER_SUPPLIES = gql`
|
||||
query GetIncomingSellerSupplies {
|
||||
incomingSellerSupplies {
|
||||
id
|
||||
status
|
||||
sellerId
|
||||
seller {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
fulfillmentCenterId
|
||||
fulfillmentCenter {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
requestedDeliveryDate
|
||||
notes
|
||||
|
||||
supplierId
|
||||
supplier {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
|
||||
totalCostWithDelivery
|
||||
|
||||
items {
|
||||
id
|
||||
product {
|
||||
id
|
||||
name
|
||||
article
|
||||
}
|
||||
requestedQuantity
|
||||
unitPrice
|
||||
totalPrice
|
||||
}
|
||||
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_MY_SELLER_SUPPLY_REQUESTS = gql`
|
||||
query GetMySellerSupplyRequests {
|
||||
mySellerSupplyRequests {
|
||||
id
|
||||
status
|
||||
sellerId
|
||||
seller {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
fulfillmentCenterId
|
||||
fulfillmentCenter {
|
||||
id
|
||||
name
|
||||
inn
|
||||
}
|
||||
requestedDeliveryDate
|
||||
notes
|
||||
|
||||
totalCostWithDelivery
|
||||
|
||||
items {
|
||||
id
|
||||
product {
|
||||
id
|
||||
name
|
||||
article
|
||||
}
|
||||
requestedQuantity
|
||||
unitPrice
|
||||
totalPrice
|
||||
}
|
||||
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// =============================================================================
|
||||
// ✏️ MUTATIONS - ИЗМЕНЕНИЕ ДАННЫХ
|
||||
// =============================================================================
|
||||
|
||||
export const CREATE_SELLER_CONSUMABLE_SUPPLY = gql`
|
||||
mutation CreateSellerConsumableSupply($input: CreateSellerConsumableSupplyInput!) {
|
||||
createSellerConsumableSupply(input: $input) {
|
||||
success
|
||||
message
|
||||
supplyOrder {
|
||||
id
|
||||
status
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const UPDATE_SELLER_SUPPLY_STATUS = gql`
|
||||
mutation UpdateSellerSupplyStatus($id: ID!, $status: SellerSupplyOrderStatus!, $notes: String) {
|
||||
updateSellerSupplyStatus(id: $id, status: $status, notes: $notes) {
|
||||
id
|
||||
status
|
||||
updatedAt
|
||||
supplierApprovedAt
|
||||
shippedAt
|
||||
deliveredAt
|
||||
supplierNotes
|
||||
receiptNotes
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const CANCEL_SELLER_SUPPLY = gql`
|
||||
mutation CancelSellerSupply($id: ID!) {
|
||||
cancelSellerSupply(id: $id) {
|
||||
id
|
||||
status
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
701
src/graphql/resolvers/seller-consumables.ts
Normal file
701
src/graphql/resolvers/seller-consumables.ts
Normal file
@ -0,0 +1,701 @@
|
||||
// =============================================================================
|
||||
// 📦 РЕЗОЛВЕРЫ ДЛЯ СИСТЕМЫ ПОСТАВОК РАСХОДНИКОВ СЕЛЛЕРА
|
||||
// =============================================================================
|
||||
|
||||
import { GraphQLError } from 'graphql'
|
||||
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { notifyOrganization } from '@/lib/realtime'
|
||||
|
||||
import { Context } from '../context'
|
||||
|
||||
// =============================================================================
|
||||
// 🔍 QUERY RESOLVERS
|
||||
// =============================================================================
|
||||
|
||||
export const sellerConsumableQueries = {
|
||||
// Мои поставки (для селлеров - заказы которые я создал)
|
||||
mySellerConsumableSupplies: 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.sellerConsumableSupplyOrder.findMany({
|
||||
where: {
|
||||
sellerId: user.organizationId!,
|
||||
},
|
||||
include: {
|
||||
seller: true,
|
||||
fulfillmentCenter: true,
|
||||
supplier: true,
|
||||
logisticsPartner: true,
|
||||
receivedBy: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
return supplies
|
||||
} catch (error) {
|
||||
console.error('Error fetching seller consumable supplies:', error)
|
||||
return [] // Возвращаем пустой массив вместо throw
|
||||
}
|
||||
},
|
||||
|
||||
// Входящие заказы от селлеров (для фулфилмента - заказы в мой ФФ)
|
||||
incomingSellerSupplies: 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.sellerConsumableSupplyOrder.findMany({
|
||||
where: {
|
||||
fulfillmentCenterId: user.organizationId!,
|
||||
},
|
||||
include: {
|
||||
seller: true,
|
||||
fulfillmentCenter: true,
|
||||
supplier: true,
|
||||
logisticsPartner: true,
|
||||
receivedBy: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
return supplies
|
||||
} catch (error) {
|
||||
console.error('Error fetching incoming seller supplies:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
// Заказы от селлеров (для поставщиков - заказы которые нужно выполнить)
|
||||
mySellerSupplyRequests: 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.sellerConsumableSupplyOrder.findMany({
|
||||
where: {
|
||||
supplierId: user.organizationId!,
|
||||
},
|
||||
include: {
|
||||
seller: true,
|
||||
fulfillmentCenter: true,
|
||||
supplier: true,
|
||||
logisticsPartner: true,
|
||||
receivedBy: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
return supplies
|
||||
} catch (error) {
|
||||
console.error('Error fetching seller supply requests:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
// Получение конкретной поставки селлера
|
||||
sellerConsumableSupply: 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.sellerConsumableSupplyOrder.findUnique({
|
||||
where: { id: args.id },
|
||||
include: {
|
||||
seller: true,
|
||||
fulfillmentCenter: true,
|
||||
supplier: true,
|
||||
logisticsPartner: true,
|
||||
receivedBy: true,
|
||||
items: {
|
||||
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 consumable supply:', error)
|
||||
throw new GraphQLError('Ошибка получения поставки')
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ✏️ MUTATION RESOLVERS
|
||||
// =============================================================================
|
||||
|
||||
export const sellerConsumableMutations = {
|
||||
// Создание поставки расходников селлера
|
||||
createSellerConsumableSupply: 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, items, notes } = args.input
|
||||
|
||||
// 🔍 ВАЛИДАЦИЯ ПАРТНЕРОВ
|
||||
|
||||
// Проверяем что фулфилмент-центр существует и является партнером
|
||||
const fulfillmentCenter = await prisma.organization.findUnique({
|
||||
where: { id: fulfillmentCenterId },
|
||||
include: {
|
||||
counterpartiesAsCounterparty: {
|
||||
where: { organizationId: user.organizationId! },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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! },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!supplier || supplier.type !== 'WHOLESALE') {
|
||||
throw new GraphQLError('Поставщик не найден или имеет неверный тип')
|
||||
}
|
||||
|
||||
if (supplier.counterpartiesAsCounterparty.length === 0) {
|
||||
throw new GraphQLError('Нет партнерских отношений с данным поставщиком')
|
||||
}
|
||||
|
||||
// 🔍 ВАЛИДАЦИЯ ТОВАРОВ И ОСТАТКОВ
|
||||
let totalAmount = 0
|
||||
const validatedItems = []
|
||||
|
||||
for (const item of items) {
|
||||
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 (product.type !== 'CONSUMABLE') {
|
||||
throw new GraphQLError(`Товар ${product.name} не является расходником`)
|
||||
}
|
||||
|
||||
// ✅ ПРОВЕРКА ОСТАТКОВ У ПОСТАВЩИКА
|
||||
const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0)
|
||||
|
||||
if (item.requestedQuantity > availableStock) {
|
||||
throw new GraphQLError(
|
||||
`Недостаточно остатков товара "${product.name}". ` +
|
||||
`Доступно: ${availableStock} шт., запрашивается: ${item.requestedQuantity} шт.`,
|
||||
)
|
||||
}
|
||||
|
||||
const itemTotalPrice = product.price.toNumber() * item.requestedQuantity
|
||||
totalAmount += itemTotalPrice
|
||||
|
||||
validatedItems.push({
|
||||
productId: item.productId,
|
||||
requestedQuantity: item.requestedQuantity,
|
||||
unitPrice: product.price,
|
||||
totalPrice: itemTotalPrice,
|
||||
})
|
||||
}
|
||||
|
||||
// 🚀 СОЗДАНИЕ ПОСТАВКИ В ТРАНЗАКЦИИ
|
||||
const supplyOrder = await prisma.$transaction(async (tx) => {
|
||||
// Создаем заказ поставки
|
||||
const newOrder = await tx.sellerConsumableSupplyOrder.create({
|
||||
data: {
|
||||
sellerId: user.organizationId!,
|
||||
fulfillmentCenterId,
|
||||
supplierId,
|
||||
logisticsPartnerId,
|
||||
requestedDeliveryDate: new Date(requestedDeliveryDate),
|
||||
notes,
|
||||
status: 'PENDING',
|
||||
totalCostWithDelivery: totalAmount,
|
||||
},
|
||||
include: {
|
||||
seller: true,
|
||||
fulfillmentCenter: true,
|
||||
supplier: true,
|
||||
logisticsPartner: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Создаем позиции заказа
|
||||
for (const item of validatedItems) {
|
||||
await tx.sellerConsumableSupplyItem.create({
|
||||
data: {
|
||||
supplyOrderId: newOrder.id,
|
||||
...item,
|
||||
},
|
||||
})
|
||||
|
||||
// Резервируем товар у поставщика (увеличиваем ordered)
|
||||
await tx.product.update({
|
||||
where: { id: item.productId },
|
||||
data: {
|
||||
ordered: {
|
||||
increment: item.requestedQuantity,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return newOrder
|
||||
})
|
||||
|
||||
// 📨 УВЕДОМЛЕНИЯ
|
||||
// Уведомляем поставщика о новом заказе
|
||||
await notifyOrganization(supplierId, `Новый заказ от селлера ${user.organization.name}`, 'SUPPLY_ORDER_CREATED', {
|
||||
orderId: supplyOrder.id,
|
||||
})
|
||||
|
||||
// Уведомляем фулфилмент о входящей поставке
|
||||
await notifyOrganization(
|
||||
fulfillmentCenterId,
|
||||
`Селлер ${user.organization.name} оформил поставку на ваш склад`,
|
||||
'INCOMING_SUPPLY_ORDER',
|
||||
{ orderId: supplyOrder.id },
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Поставка успешно создана',
|
||||
supplyOrder: await prisma.sellerConsumableSupplyOrder.findUnique({
|
||||
where: { id: supplyOrder.id },
|
||||
include: {
|
||||
seller: true,
|
||||
fulfillmentCenter: true,
|
||||
supplier: true,
|
||||
logisticsPartner: true,
|
||||
receivedBy: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating seller consumable supply:', error)
|
||||
|
||||
if (error instanceof GraphQLError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new GraphQLError('Ошибка создания поставки')
|
||||
}
|
||||
},
|
||||
|
||||
// Обновление статуса поставки (для поставщиков и фулфилмента)
|
||||
updateSellerSupplyStatus: 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.sellerConsumableSupplyOrder.findUnique({
|
||||
where: { id: args.id },
|
||||
include: {
|
||||
seller: true,
|
||||
supplier: true,
|
||||
fulfillmentCenter: 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.sellerConsumableSupplyOrder.update({
|
||||
where: { id: args.id },
|
||||
data: updateData,
|
||||
include: {
|
||||
seller: true,
|
||||
fulfillmentCenter: true,
|
||||
supplier: true,
|
||||
logisticsPartner: true,
|
||||
receivedBy: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 📨 УВЕДОМЛЕНИЯ О СМЕНЕ СТАТУСА
|
||||
if (status === 'APPROVED') {
|
||||
await notifyOrganization(
|
||||
supply.sellerId,
|
||||
`Поставка одобрена поставщиком ${user.organization.name}`,
|
||||
'SUPPLY_APPROVED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'SHIPPED') {
|
||||
await notifyOrganization(
|
||||
supply.sellerId,
|
||||
`Поставка отгружена поставщиком ${user.organization.name}`,
|
||||
'SUPPLY_SHIPPED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
|
||||
await notifyOrganization(
|
||||
supply.fulfillmentCenterId,
|
||||
'Поставка в пути. Ожидается доставка',
|
||||
'SUPPLY_IN_TRANSIT',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'DELIVERED') {
|
||||
await notifyOrganization(
|
||||
supply.sellerId,
|
||||
`Поставка доставлена в ${supply.fulfillmentCenter.name}`,
|
||||
'SUPPLY_DELIVERED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'COMPLETED') {
|
||||
// 📦 СОЗДАНИЕ РАСХОДНИКОВ НА СКЛАДЕ ФУЛФИЛМЕНТА
|
||||
|
||||
for (const item of updatedSupply.items) {
|
||||
await prisma.supply.create({
|
||||
data: {
|
||||
name: item.product.name,
|
||||
article: item.product.article || `SELLER-${item.product.id}`,
|
||||
description: `Расходники селлера ${supply.seller.name}`,
|
||||
price: item.unitPrice,
|
||||
quantity: item.receivedQuantity || item.requestedQuantity,
|
||||
currentStock: item.receivedQuantity || item.requestedQuantity,
|
||||
usedStock: 0,
|
||||
type: 'SELLER_CONSUMABLES', // ✅ Тип для селлерских расходников
|
||||
sellerOwnerId: supply.sellerId, // ✅ Владелец - селлер
|
||||
organizationId: supply.fulfillmentCenterId, // ✅ Хранитель - фулфилмент
|
||||
category: item.product.category || 'Расходники селлера',
|
||||
status: 'available',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await notifyOrganization(
|
||||
supply.sellerId,
|
||||
`Поставка завершена. Расходники размещены на складе ${supply.fulfillmentCenter.name}`,
|
||||
'SUPPLY_COMPLETED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
}
|
||||
|
||||
return updatedSupply
|
||||
} catch (error) {
|
||||
console.error('Error updating seller supply status:', error)
|
||||
|
||||
if (error instanceof GraphQLError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new GraphQLError('Ошибка обновления статуса поставки')
|
||||
}
|
||||
},
|
||||
|
||||
// Отмена поставки селлером (только PENDING/APPROVED)
|
||||
cancelSellerSupply: 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.sellerConsumableSupplyOrder.findUnique({
|
||||
where: { id: args.id },
|
||||
include: {
|
||||
seller: true,
|
||||
items: {
|
||||
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.sellerConsumableSupplyOrder.update({
|
||||
where: { id: args.id },
|
||||
data: {
|
||||
status: 'CANCELLED',
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
seller: true,
|
||||
fulfillmentCenter: true,
|
||||
supplier: true,
|
||||
items: {
|
||||
include: {
|
||||
product: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Освобождаем зарезервированные товары у поставщика
|
||||
for (const item of supply.items) {
|
||||
await tx.product.update({
|
||||
where: { id: item.productId },
|
||||
data: {
|
||||
ordered: {
|
||||
decrement: item.requestedQuantity,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return updated
|
||||
})
|
||||
|
||||
// 📨 УВЕДОМЛЕНИЯ ОБ ОТМЕНЕ
|
||||
if (supply.supplierId) {
|
||||
await notifyOrganization(
|
||||
supply.supplierId,
|
||||
`Селлер ${user.organization.name} отменил заказ`,
|
||||
'SUPPLY_CANCELLED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
}
|
||||
|
||||
await notifyOrganization(
|
||||
supply.fulfillmentCenterId,
|
||||
`Селлер ${user.organization.name} отменил поставку`,
|
||||
'SUPPLY_CANCELLED',
|
||||
{ orderId: args.id },
|
||||
)
|
||||
|
||||
return cancelledSupply
|
||||
} catch (error) {
|
||||
console.error('Error cancelling seller supply:', error)
|
||||
|
||||
if (error instanceof GraphQLError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new GraphQLError('Ошибка отмены поставки')
|
||||
}
|
||||
},
|
||||
}
|
Reference in New Issue
Block a user