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:
Veronika Smirnova
2025-08-25 23:06:10 +03:00
parent 57f8f762c9
commit 7f0e09eef6
2 changed files with 1021 additions and 0 deletions

View 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
}
}
`

View 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('Ошибка отмены поставки')
}
},
}