Files
sfera-new/docs/business-processes/SUPPLY_CHAIN_WORKFLOW.md
Veronika Smirnova 6e3201f491 feat: Phase 1 - Implementation of Data Security Infrastructure
Implemented comprehensive data security infrastructure for SFERA platform:

## Security Classes Created:
- `SupplyDataFilter`: Role-based data filtering for supply orders
- `ParticipantIsolation`: Data isolation between competing organizations
- `RecipeAccessControl`: Protection of production recipes and trade secrets
- `CommercialDataAudit`: Audit logging and suspicious activity detection
- `SecurityLogger`: Centralized security event logging system

## Infrastructure Components:
- Feature flags system for gradual security rollout
- Database migrations for audit logging (AuditLog, SecurityAlert models)
- Secure resolver wrapper for automatic GraphQL security
- TypeScript interfaces and type safety throughout

## Security Features:
- Role-based access control (SELLER, WHOLESALE, FULFILLMENT, LOGIST)
- Commercial data protection between competitors
- Production recipe confidentiality
- Audit trail for all data access
- Real-time security monitoring and alerts
- Rate limiting and suspicious activity detection

## Implementation Notes:
- All console logging replaced with centralized security logger
- Comprehensive TypeScript typing with no explicit 'any' types
- Modular architecture following SFERA coding standards
- Feature flag controlled rollout for safe deployment

This completes Phase 1 of the security implementation plan.
Next phases will integrate these classes into existing GraphQL resolvers.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 17:51:02 +03:00

35 KiB
Raw Permalink Blame History

WORKFLOW ЦЕПОЧКИ ПОСТАВОК СИСТЕМЫ SFERA

🎯 ОБЗОР СИСТЕМЫ

Система поставок SFERA работает по 8-статусной модели с участием 4 типов организаций:

  • SELLER - инициатор поставки
  • WHOLESALE - поставщик товаров
  • LOGIST - доставка
  • FULFILLMENT - получатель и обработчик

🔄 СТАТУСЫ ПОСТАВОК (SupplyOrderStatus)

graph TD
    A[PENDING] --> B[SUPPLIER_APPROVED]
    A --> X[CANCELLED]
    B --> C[LOGISTICS_CONFIRMED]
    B --> X
    C --> D[SHIPPED]
    C --> X
    D --> E[DELIVERED]
    D --> X

    F[CONFIRMED*] -.-> B
    G[IN_TRANSIT*] -.-> D

    style F fill:#f9f,stroke:#333,stroke-dasharray: 5 5
    style G fill:#f9f,stroke:#333,stroke-dasharray: 5 5

*Устаревшие статусы для обратной совместимости

📋 ДЕТАЛЬНОЕ ОПИСАНИЕ СТАТУСОВ

1. PENDING (Ожидает одобрения поставщика)

  • Инициатор: SELLER создает заказ поставки
  • Ответственный: WHOLESALE (поставщик)
  • Действия:
    • Поставщик проверяет наличие товаров
    • Подтверждает возможность поставки
    • Может отклонить заказ → CANCELLED

2. SUPPLIER_APPROVED (Поставщик одобрил)

  • Предыдущий статус: PENDING
  • Ответственный: LOGIST (логистика)
  • Действия:
    • Логистика рассчитывает маршрут и стоимость
    • Подтверждает возможность доставки
    • Планирует график забора/доставки

GraphQL мутация подтверждения поставщиком:

# Поставщик указывает детали упаковки при одобрении (опционально)
mutation SupplierApproveOrderWithPackaging($id: ID!, $packagesCount: Int, $volume: Float) {
  supplierApproveOrderWithPackaging(
    id: $id
    packagesCount: $packagesCount # Опционально: количество грузовых мест
    volume: $volume # Опционально: объём в м³ для расчета логистических тарифов
  ) {
    success
    message
    order {
      id
      status
      packagesCount # null если не указано
      volume # null если не указано
    }
  }
}

3. LOGISTICS_CONFIRMED (Логистика подтвердила)

  • Предыдущий статус: SUPPLIER_APPROVED
  • Ответственный: WHOLESALE (поставщик)
  • Действия:
    • Поставщик готовит товары к отгрузке
    • Упаковывает заказ
    • Передает логистике

Реальная мутация подтверждения логистикой:

// Из src/graphql/resolvers/logistics.ts
logisticsConfirmOrder: async (_: unknown, args: { id: string }, context: Context) => {
  if (!context.user) {
    throw new GraphQLError('Требуется авторизация')
  }

  const currentUser = await prisma.user.findUnique({
    where: { id: context.user.id },
    include: { organization: true },
  })

  // Проверка, что это логистическая компания
  if (currentUser.organization.type !== 'LOGIST') {
    throw new GraphQLError('Только логистические компании могут подтверждать заказы')
  }

  // Ищем заказ где мы назначены логистикой
  const existingOrder = await prisma.supplyOrder.findFirst({
    where: {
      id: args.id,
      logisticsPartnerId: currentUser.organization.id, // Мы - назначенная логистика
      status: 'SUPPLIER_APPROVED', // Поставщик уже одобрил
    },
  })

  if (!existingOrder) {
    throw new GraphQLError('Заказ не найден или нет доступа')
  }

  // Обновляем статус на LOGISTICS_CONFIRMED
  const updatedOrder = await prisma.supplyOrder.update({
    where: { id: args.id },
    data: { status: 'LOGISTICS_CONFIRMED' },
  })

  return {
    success: true,
    message: 'Заказ подтвержден логистикой',
    order: updatedOrder,
  }
}

4. SHIPPED (Отправлено поставщиком)

  • Предыдущий статус: LOGISTICS_CONFIRMED
  • Ответственный: LOGIST (в пути)
  • Действия:
    • Товар забран у поставщика
    • Доставка по маршруту к фулфилменту
    • Трекинг перемещения

5. DELIVERED (Доставлено и принято)

  • Предыдущий статус: SHIPPED
  • Ответственный: FULFILLMENT
  • Действия:
    • Приемка товаров на складе
    • Проверка качества и количества
    • Размещение на складе
    • ЗАВЕРШЕНИЕ WORKFLOW

Реальная реализация перехода SHIPPED → DELIVERED:

// Мутация фулфилмента для приемки товаров (из реального кода)
fulfillmentReceiveOrder: async (_: unknown, args: { id: string }, context: Context) => {
  // Проверка авторизации
  if (!context.user) {
    throw new GraphQLError('Требуется авторизация')
  }

  const currentUser = await prisma.user.findUnique({
    where: { id: context.user.id },
    include: { organization: true },
  })

  // Проверка, что это заказ для нашего фулфилмент-центра
  const existingOrder = await prisma.supplyOrder.findFirst({
    where: {
      id: args.id,
      fulfillmentCenterId: currentUser.organization.id, // Мы - получатель
      status: 'SHIPPED', // Должен быть в пути
    },
  })

  if (!existingOrder) {
    throw new GraphQLError('Заказ не найден или нет доступа')
  }

  // Обновляем статус на DELIVERED
  const updatedOrder = await prisma.supplyOrder.update({
    where: { id: args.id },
    data: { status: 'DELIVERED' },
  })

  return {
    success: true,
    message: 'Заказ успешно принят на складе',
    order: updatedOrder,
  }
}

6. CANCELLED (Отменено)

  • Может произойти на любом этапе
  • Инициатор: Любой участник процесса
  • Причины:
    • Отсутствие товаров у поставщика
    • Невозможность доставки
    • Изменение планов селлера
    • ЗАВЕРШЕНИЕ WORKFLOW

🔄 ПРАВИЛА ПЕРЕХОДОВ МЕЖДУ СТАТУСАМИ

РАЗРЕШЕННЫЕ ПЕРЕХОДЫ:

const allowedTransitions = {
  PENDING: ['SUPPLIER_APPROVED', 'CANCELLED'],
  SUPPLIER_APPROVED: ['LOGISTICS_CONFIRMED', 'CANCELLED'],
  LOGISTICS_CONFIRMED: ['SHIPPED', 'CANCELLED'],
  SHIPPED: ['DELIVERED', 'CANCELLED'],
  DELIVERED: [], // Финальный статус
  CANCELLED: [], // Финальный статус
}

ЗАПРЕЩЕННЫЕ ДЕЙСТВИЯ:

  • Возврат к предыдущим статусам
  • Пропуск промежуточных статусов
  • Изменение DELIVERED/CANCELLED заказов

🏢 РОЛИ И ОТВЕТСТВЕННОСТЬ

SELLER (Селлер-инициатор)

Создание заказа:

// Создание поставки селлером
createSupplyOrder(input: {
  partnerId: ID!           // Поставщик (WHOLESALE)
  deliveryDate: DateTime!  // Желаемая дата доставки
  fulfillmentCenterId: ID  // Фулфилмент-получатель
  logisticsPartnerId: ID   // Логистика (опционально)
})

Возможности:

  • Создавать новые заказы поставок
  • Отменять свои заказы (→ CANCELLED)
  • Просматривать статус поставок
  • Изменять статусы напрямую

WHOLESALE (Поставщик)

Обработка входящих заказов:

// Поставщик получает заказы где он является поставщиком
const supplierOrders = await prisma.supplyOrder.findMany({
  where: {
    partnerId: currentUser.organization.id, // Мы - поставщик
    status: 'PENDING', // Ожидает подтверждения
  },
})

Действия поставщика:

# Одобрение заказа
mutation SupplierApproveOrder($orderId: ID!) {
  supplierApproveOrder(id: $orderId) {
    success
    order {
      id
      status
    } # PENDING → SUPPLIER_APPROVED
  }
}

# Отклонение заказа
mutation SupplierRejectOrder($orderId: ID!, $reason: String) {
  supplierRejectOrder(id: $orderId, reason: $reason) {
    success
    message
  }
}

# Отгрузка товара (после подтверждения логистики)
mutation SupplierShipOrder($orderId: ID!) {
  supplierShipOrder(id: $orderId) {
    success
    order {
      id
      status
    } # LOGISTICS_CONFIRMED → SHIPPED
  }
}

Компоненты поставщика:

// Техническая реализация кабинета поставщика
src/components/supplier-orders/
├── supplier-orders-dashboard.tsx  # Главный dashboard
├── supplier-order-card.tsx       # Карточка заказа
├── supplier-orders-tabs.tsx      # Табы по статусам
├── supplier-orders-search.tsx    # Поиск и фильтры
└── supplier-order-stats.tsx      # Статистика заказов

Возможности:

  • Просматривать входящие заказы (PENDING)
  • Одобрять заказы (PENDING → SUPPLIER_APPROVED)
  • Отклонять заказы (PENDING → CANCELLED)
  • Отгружать товары (LOGISTICS_CONFIRMED → SHIPPED)
  • Изменять детали заказа после создания
  • Видеть заказы других поставщиков

🚨 КРИТИЧЕСКИЕ ПРОБЛЕМЫ WORKFLOW

ВЫЯВЛЕННЫЕ ПРОБЛЕМЫ В ЦЕПОЧКЕ ПОСТАВОК:

ПРОБЛЕМА 1: Неправильное отображение статусов у поставщика

// ПРОБЛЕМА: Поставщик видит "ожидает подтверждения" вместо только кнопок
// РЕШЕНИЕ: Показывать только кнопки действий, скрывать статусы

// Текущий код (неправильно):
<StatusBadge status={order.status} />
<ActionButtons />

// Правильный код:
{user?.organization?.type === 'WHOLESALE' ? (
  <ActionButtons only /> // Только кнопки, без статуса
) : (
  <StatusBadge status={order.status} />
)}

ПРОБЛЕМА 2: Отсутствие полей ввода у поставщика

// ПРОБЛЕМА: Поставщик не может указать важные данные при одобрении
interface SupplierPackagingFields {
  packagesCount?: number   // ОПЦИОНАЛЬНО: Количество грузовых мест
  volume?: number         // ОПЦИОНАЛЬНО: Объем груза для логистических расчетов
  readyDate?: DateTime    // ОПЦИОНАЛЬНО: Дата готовности к отгрузке
  notes?: string         // ОПЦИОНАЛЬНО: Комментарии для логистики
}

// ТРЕБОВАНИЯ:
// ✅ Поля НЕ обязательные - заказ можно одобрить без них
// ✅ Показываются сразу при одобрении для удобства заполнения
// ✅ Используются логистикой для расчета тарифов и планирования
// ✅ Отображаются на 1-м уровне визуализации поставки

// РЕШЕНИЕ: Расширить мутацию supplierApproveOrder
mutation SupplierApproveOrder($input: SupplierApprovalInput!) {
  supplierApproveOrder(input: $input) {
    success
    order {
      id, status, packagesCount, volume, readyDate, notes
    }
  }
}

ПРОБЛЕМА 3: Конфликт статусов в приемке фулфилмента

// КРИТИЧЕСКАЯ ОШИБКА: Резолвер ожидает SHIPPED, но получает SUPPLIER_APPROVED
if (supplyOrder.status !== 'SHIPPED') {
  return {
    success: false,
    message: 'Заказ должен быть в статусе SHIPPED для приемки', // ❌ БЛОКИРУЕТ ПРОЦЕСС
  }
}

// РЕШЕНИЕ: Исправить проверку статуса
if (!['SUPPLIER_APPROVED', 'LOGISTICS_CONFIRMED'].includes(supplyOrder.status)) {
  return {
    success: false,
    message: 'Заказ должен быть одобрен поставщиком для приемки',
  }
}

ПРОБЛЕМА 4: Отсутствие уведомлений поставщика

// ПРОБЛЕМА: Поставщик не знает о новых заказах в реальном времени
// РЕШЕНИЕ: Добавить систему уведомлений

interface SupplierNotifications {
  newOrder: 'Новый заказ от {sellerName} на сумму {amount}'
  orderCancelled: 'Заказ #{orderNumber} отменен заказчиком'
  logistics: 'Логистика подтверждена для заказа #{orderNumber}'
}

ПЛАН ИСПРАВЛЕНИЯ WORKFLOW:

interface WorkflowFixes {
  // Фаза 1: UI поставщика
  supplierInterface: {
    hideStatuses: 'Показывать только кнопки действий'
    addFields: 'Поля для packagesCount, volume, readyDate'
    realtime: 'Уведомления о новых заказах'
  }

  // Фаза 2: Backend логика
  backendLogic: {
    expandMutation: 'Расширить supplierApproveOrder с дополнительными полями'
    fixStatusCheck: 'Исправить проверку статусов в fulfillmentReceiveOrder'
    notifications: 'Система реалтайм уведомлений'
  }

  // Фаза 3: Интеграция
  integration: {
    validation: 'Валидация минимальных количеств заказа'
    inventory: 'Проверка доступности товаров у поставщика'
    logistics: 'Автоматическое назначение логистики'
  }
}

ТРЕБОВАНИЯ К РЕАЛИЗАЦИИ:

// 1. Исправленная фильтрация заказов для поставщика
const fixedSupplierFilter = `
  if (currentUser.organization.type === 'WHOLESALE') {
    whereClause = {
      partnerId: currentUser.organization.id, // Мы - поставщик
    }
  } else {
    whereClause = {
      organizationId: currentUser.organization.id, // Мы - заказчик
    }
  }
`

// 2. Правильная обработка статусов
const correctStatusHandling = `
  // Поставщик видит только кнопки, без статусов
  {userRole === 'WHOLESALE' && status === 'PENDING' && (
    <ApproveRejectButtons orderId={order.id} />
  )}
  
  // Остальные видят статусы
  {userRole !== 'WHOLESALE' && (
    <StatusBadge status={status} />
  )}
`
// Из кода resolvers.ts:
const incomingSupplierOrders = await prisma.supplyOrder.count({
  where: {
    partnerId: currentUser.organization.id, // Мы - поставщик
    status: 'PENDING', // Ожидает подтверждения от поставщика
  },
})

Возможности:

  • PENDING → SUPPLIER_APPROVED (подтверждение заказа)
  • LOGISTICS_CONFIRMED → SHIPPED (отгрузка товара)
  • Отменять заказы (→ CANCELLED)
  • Минуя логистические этапы

LOGIST (Логистика)

Обработка подтвержденных заказов:

// Из кода resolvers.ts:
const logisticsOrders = await prisma.supplyOrder.count({
  where: {
    logisticsPartnerId: currentUser.organization.id, // Мы - логистика
    status: {
      in: [
        'CONFIRMED', // Устаревший - для совместимости
        'SUPPLIER_APPROVED', // Ждет подтверждения логистики
        'LOGISTICS_CONFIRMED', // Подтверждено - нужно забрать товар
      ],
    },
  },
})

Возможности:

  • SUPPLIER_APPROVED → LOGISTICS_CONFIRMED (подтверждение логистики)
  • Планирование маршрутов доставки
  • Отменять заказы (→ CANCELLED)
  • Изменение статусов поставщика

FULFILLMENT (Получатель)

Приемка товаров:

// Фулфилмент получает:
// 1. Свои заказы расходников (ourSupplyOrders)
// 2. Заказы от селлеров (sellerSupplyOrders)

Возможности:

  • SHIPPED → DELIVERED (приемка товаров)
  • Контроль качества и количества
  • Отменять заказы (→ CANCELLED)
  • Вмешательство в процесс до доставки

📊 ТИПЫ ПОСТАВОК ПО КОНТЕНТУ

FULFILLMENT_CONSUMABLES

Описание: Расходники для операций фулфилмента

  • Инициатор: FULFILLMENT заказывает у WHOLESALE
  • Назначение: Операционные нужды (упаковка, маркировка, etc.)
  • Склад: Остается на складе фулфилмента

SELLER_CONSUMABLES

Описание: Расходники селлеров на хранении

  • Инициатор: SELLER заказывает у WHOLESALE
  • Назначение: Компоненты для продуктов селлера
  • Склад: Размещается на складе фулфилмента для селлера

PRODUCTS (Товары селлеров)

Описание: Готовые товары для отправки на маркетплейсы

  • Инициатор: SELLER заказывает у WHOLESALE
  • Назначение: Пополнение товарного запаса
  • Склад: Готовые к отправке товары

⚠️ КРИТИЧЕСКИЕ ПРАВИЛА WORKFLOW

1. ПРИНЦИП ОТВЕТСТВЕННОСТИ

Каждый статус имеет единственного ответственного за переход к следующему

2. ПРИНЦИП НЕОБРАТИМОСТИ

Невозможно вернуться к предыдущим статусам - только вперед или отмена

3. ПРИНЦИП ПРОЗРАЧНОСТИ

Все участники видят текущий статус и следующие шаги

4. ПРИНЦИП АВТОНОМНОСТИ

Каждый участник может отменить заказ на своем этапе

🔍 LEGACY СТАТУСЫ (Обратная совместимость)

CONFIRMED (устаревший)

  • Маппинг: → SUPPLIER_APPROVED
  • Причина: Переименование для ясности
  • Использование: Только в старых записях БД

IN_TRANSIT (устаревший)

  • Маппинг: → SHIPPED
  • Причина: Более точное описание статуса
  • Использование: Только в старых записях БД

🚀 ДЕТАЛЬНЫЕ МУТАЦИИ WORKFLOW (РЕАЛЬНЫЙ КОД)

Создание поставки (createSupplyOrder)

// Полная реализация из resolvers.ts:4828-4927
createSupplyOrder: async (_: unknown, args: { input: SupplyOrderInput }, context: Context) => {
  console.warn('🚀 CREATE_SUPPLY_ORDER RESOLVER - ВЫЗВАН:', {
    hasUser: !!context.user,
    userId: context.user?.id,
    inputData: args.input,
    timestamp: new Date().toISOString(),
  })

  if (!context.user) {
    throw new GraphQLError('Требуется авторизация')
  }

  const currentUser = await prisma.user.findUnique({
    where: { id: context.user.id },
    include: { organization: true },
  })

  // Проверка типа организации
  const allowedTypes = ['FULFILLMENT', 'SELLER', 'LOGIST']
  if (!allowedTypes.includes(currentUser.organization.type)) {
    throw new GraphQLError('Заказы поставок недоступны для данного типа организации')
  }

  // Определяем роль организации в процессе поставки
  const organizationRole = currentUser.organization.type
  let fulfillmentCenterId = args.input.fulfillmentCenterId

  // Если заказ создает фулфилмент-центр, он сам является получателем
  if (organizationRole === 'FULFILLMENT') {
    fulfillmentCenterId = currentUser.organization.id
  }

  // Проверяем существование фулфилмент-центра
  if (fulfillmentCenterId) {
    const fulfillmentCenter = await prisma.organization.findFirst({
      where: {
        id: fulfillmentCenterId,
        type: 'FULFILLMENT',
      },
    })

    if (!fulfillmentCenter) {
      return {
        success: false,
        message: 'Указанный фулфилмент-центр не найден',
      }
    }
  }

  // Создание заказа с проверкой партнерских связей...
}

Универсальное обновление статуса (updateSupplyOrderStatus)

// Реализация из resolvers.ts:6900-6950
updateSupplyOrderStatus: async (_: unknown, args: { id: string; status: SupplyOrderStatus }, context: Context) => {
  console.warn(`[DEBUG] updateSupplyOrderStatus вызван для заказа ${args.id} со статусом ${args.status}`)

  if (!context.user) {
    throw new GraphQLError('Требуется авторизация')
  }

  const currentUser = await prisma.user.findUnique({
    where: { id: context.user.id },
    include: { organization: true },
  })

  // Находим заказ поставки с проверкой доступа
  const existingOrder = await prisma.supplyOrder.findFirst({
    where: {
      id: args.id,
      OR: [
        { organizationId: currentUser.organization.id }, // Создатель заказа
        { partnerId: currentUser.organization.id }, // Поставщик
        { fulfillmentCenterId: currentUser.organization.id }, // Фулфилмент-центр
        { logisticsPartnerId: currentUser.organization.id }, // Логистика
      ],
    },
    include: {
      items: {
        include: {
          product: {
            include: { category: true },
          },
        },
      },
      organization: true,
      partner: true,
      fulfillmentCenter: true,
      logisticsPartner: true,
    },
  })

  if (!existingOrder) {
    return {
      success: false,
      message: 'Заказ не найден или нет доступа к этому заказу',
    }
  }

  // БИЗНЕС-ПРАВИЛА ПЕРЕХОДОВ СТАТУСОВ
  const validateStatusTransition = (currentStatus: string, newStatus: string, userOrgType: string) => {
    const transitions = {
      PENDING: {
        SUPPLIER_APPROVED: ['WHOLESALE'], // Только поставщик может одобрить
        CANCELLED: ['SELLER', 'WHOLESALE', 'FULFILLMENT'], // Участники могут отменить
      },
      SUPPLIER_APPROVED: {
        LOGISTICS_CONFIRMED: ['LOGIST'], // Только логистика может подтвердить
        CANCELLED: ['WHOLESALE', 'LOGIST', 'FULFILLMENT'],
      },
      LOGISTICS_CONFIRMED: {
        SHIPPED: ['WHOLESALE'], // Только поставщик может отгрузить
        CANCELLED: ['WHOLESALE', 'LOGIST', 'FULFILLMENT'],
      },
      SHIPPED: {
        DELIVERED: ['FULFILLMENT'], // Только фулфилмент может принять
        CANCELLED: ['LOGIST', 'FULFILLMENT'], // В крайних случаях
      },
    }

    const allowedRoles = transitions[currentStatus]?.[newStatus]
    if (!allowedRoles || !allowedRoles.includes(userOrgType)) {
      throw new GraphQLError(`Переход ${currentStatus}${newStatus} недоступен для организации типа ${userOrgType}`)
    }
  }

  // Валидируем переход статуса
  validateStatusTransition(existingOrder.status, args.status, currentUser.organization.type)

  // Обновляем статус заказа
  const updatedOrder = await prisma.supplyOrder.update({
    where: { id: args.id },
    data: { status: args.status },
    include: {
      items: {
        include: {
          product: {
            include: { category: true },
          },
        },
      },
      organization: true,
      partner: true,
      fulfillmentCenter: true,
      logisticsPartner: true,
    },
  })

  return {
    success: true,
    message: `Статус заказа успешно изменен на ${args.status}`,
    order: updatedOrder,
  }
}

Подтверждение логистики (logisticsConfirmOrder)

// Реализация из resolvers.ts:7681-7720
logisticsConfirmOrder: async (_: unknown, args: { id: string }, context: Context) => {
  if (!context.user) {
    throw new GraphQLError('Требуется авторизация')
  }

  const currentUser = await prisma.user.findUnique({
    where: { id: context.user.id },
    include: { organization: true },
  })

  // ПРОВЕРКА РОЛИ: только логистические компании
  if (currentUser.organization.type !== 'LOGIST') {
    throw new GraphQLError('Только логистические компании могут подтверждать заказы')
  }

  // Ищем заказ где мы назначены логистикой
  const existingOrder = await prisma.supplyOrder.findFirst({
    where: {
      id: args.id,
      logisticsPartnerId: currentUser.organization.id, // Мы - назначенная логистика
      status: 'SUPPLIER_APPROVED', // Поставщик уже одобрил
    },
    include: {
      organization: true,
      partner: true,
      fulfillmentCenter: true,
    },
  })

  if (!existingOrder) {
    return {
      success: false,
      message: 'Заказ не найден, не назначен вашей компании, или находится в неподходящем статусе',
    }
  }

  // БИЗНЕС-ЛОГИКА: обновляем статус на LOGISTICS_CONFIRMED
  const updatedOrder = await prisma.supplyOrder.update({
    where: { id: args.id },
    data: { status: 'LOGISTICS_CONFIRMED' },
    include: {
      items: {
        include: {
          product: true,
        },
      },
      organization: true,
      partner: true,
      fulfillmentCenter: true,
      logisticsPartner: true,
    },
  })

  return {
    success: true,
    message: 'Заказ подтвержден логистической компанией. Поставщик может приступать к отгрузке.',
    order: updatedOrder,
  }
}

Создание поставки Wildberries (createWildberriesSupply)

// Специализированная мутация для маркетплейса WB (из resolvers.ts:6772-6800)
createWildberriesSupply: async (_: unknown, args: { input: WildberriesSupplyInput }, context: Context) => {
  if (!context.user) {
    throw new GraphQLError('Требуется авторизация')
  }

  const currentUser = await prisma.user.findUnique({
    where: { id: context.user.id },
    include: { organization: true },
  })

  if (!currentUser?.organization) {
    throw new GraphQLError('У пользователя нет организации')
  }

  // ПРОВЕРКА ТИПА: только селлеры могут создавать поставки WB
  if (currentUser.organization.type !== 'SELLER') {
    throw new GraphQLError('Поставки Wildberries доступны только для селлеров')
  }

  try {
    // БИЗНЕС-ЛОГИКА: создание специализированной поставки для WB
    const supplyData = {
      organizationId: currentUser.organization.id,
      type: 'WILDBERRIES_SUPPLY',
      status: 'PENDING',
      cards: args.input.cards.map((card) => ({
        price: card.price,
        discountedPrice: card.discountedPrice,
        selectedQuantity: card.selectedQuantity,
        selectedServices: card.selectedServices || [],
      })),
      createdAt: new Date(),
    }

    // Интеграция с API Wildberries для создания поставки...

    return {
      success: true,
      message: 'Поставка Wildberries успешно создана',
      supply: supplyData,
    }
  } catch (error) {
    console.error('Ошибка создания поставки WB:', error)
    return {
      success: false,
      message: 'Ошибка при создании поставки Wildberries',
    }
  }
}

📋 СИСТЕМА СЧЕТЧИКОВ ПО РОЛЯМ

Динамические счетчики для UI (из реального кода)

// Логика подсчета pending заказов по типам организаций (resolvers.ts:850-950)
let pendingSupplyOrders = 0

if (currentUser.organization.type === 'FULFILLMENT') {
  // ДЛЯ ФУЛФИЛМЕНТА: собственные + заказы от селлеров
  const ourSupplyOrders = await prisma.supplyOrder.count({
    where: {
      organizationId: currentUser.organization.id, // Мы создали заказ
      status: { in: ['PENDING', 'SUPPLIER_APPROVED', 'LOGISTICS_CONFIRMED', 'SHIPPED'] },
    },
  })

  const sellerSupplyOrders = await prisma.supplyOrder.count({
    where: {
      fulfillmentCenterId: currentUser.organization.id, // Мы - получатель
      organizationId: { not: currentUser.organization.id }, // Не наши заказы
      status: { in: ['PENDING', 'SUPPLIER_APPROVED', 'LOGISTICS_CONFIRMED', 'SHIPPED'] },
    },
  })

  pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders
} else if (currentUser.organization.type === 'WHOLESALE') {
  // ДЛЯ ПОСТАВЩИКА: входящие заказы для подтверждения
  const incomingSupplierOrders = await prisma.supplyOrder.count({
    where: {
      partnerId: currentUser.organization.id, // Мы - поставщик
      status: 'PENDING', // Ожидает подтверждения от поставщика
    },
  })

  pendingSupplyOrders = incomingSupplierOrders
} else if (currentUser.organization.type === 'LOGIST') {
  // ДЛЯ ЛОГИСТИКИ: заказы требующие действий
  const logisticsOrders = await prisma.supplyOrder.count({
    where: {
      logisticsPartnerId: currentUser.organization.id, // Мы - логистика
      status: {
        in: [
          'CONFIRMED', // Legacy: Подтверждено фулфилментом
          'SUPPLIER_APPROVED', // Подтверждено поставщиком - нужно подтвердить логистикой
          'LOGISTICS_CONFIRMED', // Подтверждено логистикой - нужно забрать товар
        ],
      },
    },
  })

  pendingSupplyOrders = logisticsOrders
} else if (currentUser.organization.type === 'SELLER') {
  // ДЛЯ СЕЛЛЕРА: созданные заказы в процессе
  const sellerOrders = await prisma.supplyOrder.count({
    where: {
      organizationId: currentUser.organization.id, // Мы создали заказ
      status: { in: ['PENDING', 'SUPPLIER_APPROVED', 'LOGISTICS_CONFIRMED', 'SHIPPED'] },
    },
  })

  pendingSupplyOrders = sellerOrders
}

🔄 РАСШИРЕННЫЕ ПРАВИЛА СТАТУСНЫХ ПЕРЕХОДОВ

Матрица доступных действий

// Карта доступных действий по статусам и ролям
const statusActionMatrix = {
  PENDING: {
    WHOLESALE: ['approve', 'cancel', 'add_packaging_details'], // Поставщик может одобрить или отменить
    SELLER: ['cancel', 'modify'], // Селлер может отменить или изменить
    FULFILLMENT: ['cancel'], // ФФ может отменить свои заказы
    LOGIST: [], // Логистика не участвует на этом этапе
  },

  SUPPLIER_APPROVED: {
    WHOLESALE: ['cancel', 'update_packaging'], // Поставщик может отменить или уточнить упаковку
    LOGIST: ['confirm', 'cancel', 'set_route'], // Логистика может подтвердить или отменить
    SELLER: ['cancel'], // Селлер может отменить
    FULFILLMENT: ['cancel'], // ФФ может отменить
  },

  LOGISTICS_CONFIRMED: {
    WHOLESALE: ['ship', 'cancel'], // Поставщик может отгрузить или отменить
    LOGIST: ['cancel', 'update_route'], // Логистика может отменить или изменить маршрут
    SELLER: ['cancel'], // Селлер может отменить
    FULFILLMENT: ['cancel'], // ФФ может отменить
  },

  SHIPPED: {
    FULFILLMENT: ['receive', 'report_issues'], // ФФ может принять или сообщить о проблемах
    LOGIST: ['update_tracking', 'report_delay'], // Логистика может обновить трекинг
    WHOLESALE: [], // Поставщик ждет
    SELLER: [], // Селлер ждет
  },

  DELIVERED: {
    // Финальный статус - никто не может изменить
  },

  CANCELLED: {
    // Финальный статус - никто не может изменить
  },
}

Дополнено реальными мутациями из кода: createSupplyOrder, updateSupplyOrderStatus, logisticsConfirmOrder, createWildberriesSupply
Источники: src/graphql/resolvers.ts:4828+, 6900+, 7681+, 6772+ Обновлено: 2025-08-21