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