# WORKFLOW ЦЕПОЧКИ ПОСТАВОК СИСТЕМЫ SFERA ## 🎯 ОБЗОР СИСТЕМЫ Система поставок SFERA работает по 8-статусной модели с участием 4 типов организаций: - **SELLER** - инициатор поставки - **WHOLESALE** - поставщик товаров - **LOGIST** - доставка - **FULFILLMENT** - получатель и обработчик ## 🔄 СТАТУСЫ ПОСТАВОК (SupplyOrderStatus) ```mermaid 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 мутация подтверждения поставщиком:** ```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 (поставщик) - **Действия**: - Поставщик готовит товары к отгрузке - Упаковывает заказ - Передает логистике **Реальная мутация подтверждения логистикой:** ```typescript // Из 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:** ```typescript // Мутация фулфилмента для приемки товаров (из реального кода) 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** ## 🔄 ПРАВИЛА ПЕРЕХОДОВ МЕЖДУ СТАТУСАМИ ### РАЗРЕШЕННЫЕ ПЕРЕХОДЫ: ```typescript const allowedTransitions = { PENDING: ['SUPPLIER_APPROVED', 'CANCELLED'], SUPPLIER_APPROVED: ['LOGISTICS_CONFIRMED', 'CANCELLED'], LOGISTICS_CONFIRMED: ['SHIPPED', 'CANCELLED'], SHIPPED: ['DELIVERED', 'CANCELLED'], DELIVERED: [], // Финальный статус CANCELLED: [], // Финальный статус } ``` ### ЗАПРЕЩЕННЫЕ ДЕЙСТВИЯ: - ❌ Возврат к предыдущим статусам - ❌ Пропуск промежуточных статусов - ❌ Изменение DELIVERED/CANCELLED заказов ## 🏢 РОЛИ И ОТВЕТСТВЕННОСТЬ ### SELLER (Селлер-инициатор) **Создание заказа:** ```typescript // Создание поставки селлером createSupplyOrder(input: { partnerId: ID! // Поставщик (WHOLESALE) deliveryDate: DateTime! // Желаемая дата доставки fulfillmentCenterId: ID // Фулфилмент-получатель logisticsPartnerId: ID // Логистика (опционально) }) ``` **Возможности:** - ✅ Создавать новые заказы поставок - ✅ Отменять свои заказы (→ CANCELLED) - ✅ Просматривать статус поставок - ❌ Изменять статусы напрямую ### WHOLESALE (Поставщик) **Обработка входящих заказов:** ```typescript // Поставщик получает заказы где он является поставщиком const supplierOrders = await prisma.supplyOrder.findMany({ where: { partnerId: currentUser.organization.id, // Мы - поставщик status: 'PENDING', // Ожидает подтверждения }, }) ``` **Действия поставщика:** ```graphql # Одобрение заказа 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 } } ``` **Компоненты поставщика:** ```typescript // Техническая реализация кабинета поставщика 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: Неправильное отображение статусов у поставщика** ```typescript // ПРОБЛЕМА: Поставщик видит "ожидает подтверждения" вместо только кнопок // РЕШЕНИЕ: Показывать только кнопки действий, скрывать статусы // Текущий код (неправильно): // Правильный код: {user?.organization?.type === 'WHOLESALE' ? ( // Только кнопки, без статуса ) : ( )} ``` #### ❌ **ПРОБЛЕМА 2: Отсутствие полей ввода у поставщика** ```typescript // ПРОБЛЕМА: Поставщик не может указать важные данные при одобрении 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: Конфликт статусов в приемке фулфилмента** ```typescript // КРИТИЧЕСКАЯ ОШИБКА: Резолвер ожидает 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: Отсутствие уведомлений поставщика** ```typescript // ПРОБЛЕМА: Поставщик не знает о новых заказах в реальном времени // РЕШЕНИЕ: Добавить систему уведомлений interface SupplierNotifications { newOrder: 'Новый заказ от {sellerName} на сумму {amount}' orderCancelled: 'Заказ #{orderNumber} отменен заказчиком' logistics: 'Логистика подтверждена для заказа #{orderNumber}' } ``` ### ПЛАН ИСПРАВЛЕНИЯ WORKFLOW: ```typescript interface WorkflowFixes { // Фаза 1: UI поставщика supplierInterface: { hideStatuses: 'Показывать только кнопки действий' addFields: 'Поля для packagesCount, volume, readyDate' realtime: 'Уведомления о новых заказах' } // Фаза 2: Backend логика backendLogic: { expandMutation: 'Расширить supplierApproveOrder с дополнительными полями' fixStatusCheck: 'Исправить проверку статусов в fulfillmentReceiveOrder' notifications: 'Система реалтайм уведомлений' } // Фаза 3: Интеграция integration: { validation: 'Валидация минимальных количеств заказа' inventory: 'Проверка доступности товаров у поставщика' logistics: 'Автоматическое назначение логистики' } } ``` ### ТРЕБОВАНИЯ К РЕАЛИЗАЦИИ: ```typescript // 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' && ( )} // Остальные видят статусы {userRole !== 'WHOLESALE' && ( )} ` ``` ```typescript // Из кода resolvers.ts: const incomingSupplierOrders = await prisma.supplyOrder.count({ where: { partnerId: currentUser.organization.id, // Мы - поставщик status: 'PENDING', // Ожидает подтверждения от поставщика }, }) ``` **Возможности:** - ✅ PENDING → SUPPLIER_APPROVED (подтверждение заказа) - ✅ LOGISTICS_CONFIRMED → SHIPPED (отгрузка товара) - ✅ Отменять заказы (→ CANCELLED) - ❌ Минуя логистические этапы ### LOGIST (Логистика) **Обработка подтвержденных заказов:** ```typescript // Из кода 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 (Получатель) **Приемка товаров:** ```typescript // Фулфилмент получает: // 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) ```typescript // Полная реализация из 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) ```typescript // Реализация из 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) ```typescript // Реализация из 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) ```typescript // Специализированная мутация для маркетплейса 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 (из реального кода) ```typescript // Логика подсчета 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 } ``` ## 🔄 РАСШИРЕННЫЕ ПРАВИЛА СТАТУСНЫХ ПЕРЕХОДОВ ### Матрица доступных действий ```typescript // Карта доступных действий по статусам и ролям 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_