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

974 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
// ПРОБЛЕМА: Поставщик видит "ожидает подтверждения" вместо только кнопок
// РЕШЕНИЕ: Показывать только кнопки действий, скрывать статусы
// Текущий код (неправильно):
<StatusBadge status={order.status} />
<ActionButtons />
// Правильный код:
{user?.organization?.type === 'WHOLESALE' ? (
<ActionButtons only /> // Только кнопки, без статуса
) : (
<StatusBadge status={order.status} />
)}
```
#### ❌ **ПРОБЛЕМА 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' && (
<ApproveRejectButtons orderId={order.id} />
)}
// Остальные видят статусы
{userRole !== 'WHOLESALE' && (
<StatusBadge status={status} />
)}
`
```
```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_