
🏗️ АРХИТЕКТУРНОЕ УЛУЧШЕНИЕ: Полная система модульных резолверов V2 - ✅ Добавлены 21 доменный резолвер в src/graphql/resolvers/domains/ - ✅ Добавлены 4 общих резолвера в src/graphql/resolvers/shared/ - ✅ Реализована изолированная доменно-ориентированная архитектура - ✅ Подготовлена инфраструктура для полной миграции V1→V2 📦 НОВЫЕ ДОМЕНЫ: - admin-tools, analytics, cart, catalog - counterparty-management, employee, external-ads - file-management, logistics, messaging - organization-management, products, referrals - seller-consumables, seller-goods, services - supplies, supply-orders, user-management - wildberries, logistics-consumables 🛠️ ОБЩИЕ КОМПОНЕНТЫ: - api-keys, auth-utils, scalars, types - Безопасная интеграция с существующей системой 🔗 ИНТЕГРАЦИЯ: Все резолверы готовы к подключению через src/graphql/resolvers/index.ts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
571 lines
18 KiB
TypeScript
571 lines
18 KiB
TypeScript
import { GraphQLError } from 'graphql'
|
||
|
||
import { Context } from '../../context'
|
||
import { prisma } from '../../../lib/prisma'
|
||
import { DomainResolvers } from '../shared/types'
|
||
|
||
// Logistics Domain Resolvers - управление логистикой и маршрутами
|
||
export const logisticsDomainResolvers: DomainResolvers = {
|
||
Query: {
|
||
// Логистика организации
|
||
myLogistics: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
const organizationId = currentUser.organization.id
|
||
|
||
console.warn('🚚 MY_LOGISTICS RESOLVER CALLED:', {
|
||
userId: context.user.id,
|
||
organizationId,
|
||
organizationType: currentUser.organization.type,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
// Получаем логистические маршруты организации
|
||
const logistics = await prisma.logistics.findMany({
|
||
where: { organizationId },
|
||
include: {
|
||
organization: {
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
fullName: true,
|
||
type: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
console.warn('📊 MY_LOGISTICS RESULT:', {
|
||
logisticsCount: logistics.length,
|
||
organizationType: currentUser.organization.type,
|
||
})
|
||
|
||
return logistics
|
||
},
|
||
|
||
// Логистика конкретной организации (для партнеров)
|
||
organizationLogistics: async (_: unknown, args: { organizationId: string }, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
console.warn('🏢 ORGANIZATION_LOGISTICS RESOLVER CALLED:', {
|
||
requestedOrganizationId: args.organizationId,
|
||
currentOrganizationId: currentUser.organization.id,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
// Проверяем права доступа - только партнеры или сама организация
|
||
let hasAccess = false
|
||
|
||
// Если запрашиваем свою логистику
|
||
if (args.organizationId === currentUser.organization.id) {
|
||
hasAccess = true
|
||
} else {
|
||
// Проверяем, есть ли партнерство
|
||
const partnership = await prisma.counterparty.findFirst({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterpartyId: args.organizationId,
|
||
},
|
||
})
|
||
hasAccess = !!partnership
|
||
}
|
||
|
||
if (!hasAccess) {
|
||
throw new GraphQLError('Нет доступа к логистике этой организации', {
|
||
extensions: { code: 'FORBIDDEN' },
|
||
})
|
||
}
|
||
|
||
// Получаем логистические маршруты организации
|
||
const logistics = await prisma.logistics.findMany({
|
||
where: { organizationId: args.organizationId },
|
||
include: {
|
||
organization: {
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
fullName: true,
|
||
type: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: { createdAt: 'desc' },
|
||
})
|
||
|
||
return logistics
|
||
},
|
||
|
||
// Логистические партнеры (организации-логисты)
|
||
logisticsPartners: async (_: unknown, __: unknown, context: Context) => {
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
console.warn('📦 LOGISTICS_PARTNERS RESOLVER CALLED:', {
|
||
organizationId: currentUser.organization.id,
|
||
organizationType: currentUser.organization.type,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
// Получаем логистические компании среди контрагентов
|
||
const logisticsPartners = await prisma.counterparty.findMany({
|
||
where: {
|
||
organizationId: currentUser.organization.id,
|
||
counterparty: {
|
||
type: 'LOGIST',
|
||
},
|
||
},
|
||
include: {
|
||
counterparty: {
|
||
include: {
|
||
users: true,
|
||
apiKeys: true,
|
||
},
|
||
},
|
||
},
|
||
orderBy: {
|
||
createdAt: 'desc',
|
||
},
|
||
})
|
||
|
||
const partners = logisticsPartners.map((partnership) => partnership.counterparty)
|
||
|
||
console.warn('📊 LOGISTICS_PARTNERS RESULT:', {
|
||
partnersCount: partners.length,
|
||
organizationType: currentUser.organization.type,
|
||
})
|
||
|
||
return partners
|
||
},
|
||
},
|
||
|
||
Mutation: {
|
||
// Создать логистический маршрут
|
||
createLogistics: async (
|
||
_: unknown,
|
||
args: {
|
||
input: {
|
||
fromAddress: string
|
||
toAddress: string
|
||
fromCoordinates?: string
|
||
toCoordinates?: string
|
||
distance?: number
|
||
estimatedTime?: number
|
||
cost?: number
|
||
vehicleType?: string
|
||
maxWeight?: number
|
||
maxVolume?: number
|
||
notes?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
console.warn('🆕 CREATE_LOGISTICS - ВЫЗВАН:', {
|
||
fromAddress: args.input.fromAddress,
|
||
toAddress: args.input.toAddress,
|
||
vehicleType: args.input.vehicleType,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
// Только логистические компании могут создавать маршруты
|
||
if (currentUser.organization.type !== 'LOGIST') {
|
||
throw new GraphQLError('Только логистические компании могут создавать маршруты')
|
||
}
|
||
|
||
try {
|
||
const logistics = await prisma.logistics.create({
|
||
data: {
|
||
organizationId: currentUser.organization.id,
|
||
fromAddress: args.input.fromAddress,
|
||
toAddress: args.input.toAddress,
|
||
fromCoordinates: args.input.fromCoordinates,
|
||
toCoordinates: args.input.toCoordinates,
|
||
distance: args.input.distance,
|
||
estimatedTime: args.input.estimatedTime,
|
||
cost: args.input.cost,
|
||
vehicleType: args.input.vehicleType,
|
||
maxWeight: args.input.maxWeight,
|
||
maxVolume: args.input.maxVolume,
|
||
notes: args.input.notes,
|
||
},
|
||
include: {
|
||
organization: {
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
fullName: true,
|
||
type: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
console.warn('✅ ЛОГИСТИЧЕСКИЙ МАРШРУТ СОЗДАН:', {
|
||
logisticsId: logistics.id,
|
||
organizationId: currentUser.organization.id,
|
||
fromAddress: args.input.fromAddress,
|
||
toAddress: args.input.toAddress,
|
||
cost: args.input.cost,
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Логистический маршрут успешно создан',
|
||
logistics,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating logistics:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при создании логистического маршрута',
|
||
logistics: null,
|
||
}
|
||
}
|
||
},
|
||
|
||
// Обновить логистический маршрут
|
||
updateLogistics: async (
|
||
_: unknown,
|
||
args: {
|
||
id: string
|
||
input: {
|
||
fromAddress?: string
|
||
toAddress?: string
|
||
fromCoordinates?: string
|
||
toCoordinates?: string
|
||
distance?: number
|
||
estimatedTime?: number
|
||
cost?: number
|
||
vehicleType?: string
|
||
maxWeight?: number
|
||
maxVolume?: number
|
||
notes?: string
|
||
}
|
||
},
|
||
context: Context,
|
||
) => {
|
||
console.warn('📝 UPDATE_LOGISTICS - ВЫЗВАН:', {
|
||
logisticsId: args.id,
|
||
hasUpdates: Object.keys(args.input).length,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что маршрут принадлежит текущей организации
|
||
const existingLogistics = await prisma.logistics.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingLogistics) {
|
||
return {
|
||
success: false,
|
||
message: 'Логистический маршрут не найден или нет доступа',
|
||
logistics: null,
|
||
}
|
||
}
|
||
|
||
// Обновляем маршрут
|
||
const updatedLogistics = await prisma.logistics.update({
|
||
where: { id: args.id },
|
||
data: {
|
||
...args.input,
|
||
updatedAt: new Date(),
|
||
},
|
||
include: {
|
||
organization: {
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
fullName: true,
|
||
type: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
console.warn('✅ ЛОГИСТИЧЕСКИЙ МАРШРУТ ОБНОВЛЕН:', {
|
||
logisticsId: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
updatedFields: Object.keys(args.input),
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Логистический маршрут успешно обновлен',
|
||
logistics: updatedLogistics,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating logistics:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при обновлении логистического маршрута',
|
||
logistics: null,
|
||
}
|
||
}
|
||
},
|
||
|
||
// Удалить логистический маршрут
|
||
deleteLogistics: async (_: unknown, args: { id: string }, context: Context) => {
|
||
console.warn('🗑️ DELETE_LOGISTICS - ВЫЗВАН:', {
|
||
logisticsId: args.id,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что маршрут принадлежит текущей организации
|
||
const existingLogistics = await prisma.logistics.findFirst({
|
||
where: {
|
||
id: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!existingLogistics) {
|
||
return {
|
||
success: false,
|
||
message: 'Логистический маршрут не найден или нет доступа',
|
||
}
|
||
}
|
||
|
||
// Проверяем, что маршрут не используется в активных заказах
|
||
const activeSupplyOrders = await prisma.supplyOrder.findMany({
|
||
where: {
|
||
logisticsPartnerId: currentUser.organization.id,
|
||
status: { in: ['CONFIRMED', 'IN_TRANSIT'] },
|
||
},
|
||
})
|
||
|
||
if (activeSupplyOrders.length > 0) {
|
||
return {
|
||
success: false,
|
||
message: 'Нельзя удалить маршрут, используемый в активных заказах',
|
||
}
|
||
}
|
||
|
||
// Удаляем маршрут
|
||
await prisma.logistics.delete({
|
||
where: { id: args.id },
|
||
})
|
||
|
||
console.warn('🗑️ ЛОГИСТИЧЕСКИЙ МАРШРУТ УДАЛЕН:', {
|
||
logisticsId: args.id,
|
||
organizationId: currentUser.organization.id,
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Логистический маршрут успешно удален',
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting logistics:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при удалении логистического маршрута',
|
||
}
|
||
}
|
||
},
|
||
|
||
// Назначить логистику к заказу (дублируется из supplies, но с другой логикой)
|
||
assignLogisticsToSupply: async (
|
||
_: unknown,
|
||
args: {
|
||
supplyOrderId: string
|
||
logisticsId: string
|
||
estimatedDeliveryDate?: string
|
||
specialInstructions?: string
|
||
},
|
||
context: Context,
|
||
) => {
|
||
console.warn('🚛 ASSIGN_LOGISTICS - ВЫЗВАН:', {
|
||
supplyOrderId: args.supplyOrderId,
|
||
logisticsId: args.logisticsId,
|
||
timestamp: new Date().toISOString(),
|
||
})
|
||
|
||
if (!context.user) {
|
||
throw new GraphQLError('Требуется авторизация', {
|
||
extensions: { code: 'UNAUTHENTICATED' },
|
||
})
|
||
}
|
||
|
||
const currentUser = await prisma.user.findUnique({
|
||
where: { id: context.user.id },
|
||
include: { organization: true },
|
||
})
|
||
|
||
if (!currentUser?.organization) {
|
||
throw new GraphQLError('У пользователя нет организации')
|
||
}
|
||
|
||
try {
|
||
// Проверяем, что это логистическая компания
|
||
if (currentUser.organization.type !== 'LOGIST') {
|
||
throw new GraphQLError('Только логистические компании могут назначать свои маршруты')
|
||
}
|
||
|
||
// Проверяем заказ поставки
|
||
const supplyOrder = await prisma.supplyOrder.findFirst({
|
||
where: {
|
||
id: args.supplyOrderId,
|
||
status: 'CONFIRMED',
|
||
logisticsPartnerId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!supplyOrder) {
|
||
return {
|
||
success: false,
|
||
message: 'Заказ поставки не найден или не назначен этой логистической компании',
|
||
supplyOrder: null,
|
||
}
|
||
}
|
||
|
||
// Проверяем логистический маршрут
|
||
const logistics = await prisma.logistics.findFirst({
|
||
where: {
|
||
id: args.logisticsId,
|
||
organizationId: currentUser.organization.id,
|
||
},
|
||
})
|
||
|
||
if (!logistics) {
|
||
return {
|
||
success: false,
|
||
message: 'Логистический маршрут не найден',
|
||
supplyOrder: null,
|
||
}
|
||
}
|
||
|
||
// Назначаем маршрут и обновляем статус
|
||
const updatedSupplyOrder = await prisma.supplyOrder.update({
|
||
where: { id: args.supplyOrderId },
|
||
data: {
|
||
// TODO: добавить поле logisticsId когда будет в модели
|
||
deliveryDate: args.estimatedDeliveryDate
|
||
? new Date(args.estimatedDeliveryDate)
|
||
: supplyOrder.deliveryDate,
|
||
status: 'IN_TRANSIT',
|
||
notes: args.specialInstructions || supplyOrder.notes,
|
||
updatedAt: new Date(),
|
||
},
|
||
include: {
|
||
organization: true,
|
||
fulfillmentCenter: true,
|
||
logisticsPartner: true,
|
||
items: {
|
||
include: {
|
||
product: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
console.warn('✅ ЛОГИСТИКА НАЗНАЧЕНА НА ЗАКАЗ:', {
|
||
supplyOrderId: args.supplyOrderId,
|
||
logisticsId: args.logisticsId,
|
||
logisticsCompany: currentUser.organization.name || currentUser.organization.fullName,
|
||
route: `${logistics.fromAddress} → ${logistics.toAddress}`,
|
||
})
|
||
|
||
return {
|
||
success: true,
|
||
message: 'Логистический маршрут назначен на заказ',
|
||
supplyOrder: updatedSupplyOrder,
|
||
}
|
||
} catch (error) {
|
||
console.error('Error assigning logistics:', error)
|
||
return {
|
||
success: false,
|
||
message: 'Ошибка при назначении логистического маршрута',
|
||
supplyOrder: null,
|
||
}
|
||
}
|
||
},
|
||
},
|
||
} |