# ПРАВИЛА БЕЗОПАСНОСТИ ДАННЫХ В ПОСТАВКАХ SFERA

## 🎯 ОБЗОР

Система безопасности данных в поставках обеспечивает **коммерческую конфиденциальность** и **изоляцию данных** между участниками цепочки поставок: SELLER, WHOLESALE, FULFILLMENT, LOGIST.

### КЛЮЧЕВЫЕ ПРИНЦИПЫ:

1. **Принцип минимальных привилегий** - каждый участник видит только необходимые данные
2. **Коммерческая тайна** - защита закупочных цен и производственных секретов
3. **Изоляция данных** - участники не видят данные друг друга
4. **Аудит доступа** - логирование всех обращений к чувствительным данным

## 🔐 МАТРИЦА ДОСТУПА К ДАННЫМ

### СТРУКТУРА ДАННЫХ ПОСТАВКИ:

```typescript
interface SupplyOrder {
  // Базовая информация (видна всем участникам)
  id: string
  status: SupplyOrderStatus
  deliveryDate: Date
  totalItems: number

  // Коммерческая информация (ограниченный доступ)
  productPrice: Decimal // Закупочная цена у поставщика
  fulfillmentServicePrice: Decimal // Стоимость услуг ФФ
  logisticsPrice: Decimal // Стоимость доставки
  totalAmount: Decimal // Общая сумма

  // Производственная информация (ограниченный доступ)
  recipe: {
    services: Service[] // Услуги ФФ
    fulfillmentConsumables: Supply[] // Расходники ФФ
    sellerConsumables: Supply[] // Расходники селлера
  }

  // Параметры поставки (опциональные)
  packagesCount?: number // Количество грузовых мест
  volume?: number // Объем груза в м³
  readyDate?: Date // Дата готовности к отгрузке
  notes?: string // Комментарии
}
```

### ТАБЛИЦА ДОСТУПА:

| Данные                             | SELLER | WHOLESALE | FULFILLMENT | LOGIST |
| ---------------------------------- | ------ | --------- | ----------- | ------ |
| **Базовая информация**             | ✅     | ✅        | ✅          | ✅     |
| **productPrice** (закупочная цена) | ✅     | ✅        | ❌          | ❌     |
| **fulfillmentServicePrice**        | ✅     | ❌        | ✅          | ❌     |
| **logisticsPrice**                 | ✅     | ❌        | ✅          | ✅     |
| **totalAmount для SELLER**         | ✅     | ❌        | ❌          | ❌     |
| **totalAmount для FULFILLMENT**    | ❌     | ❌        | ✅          | ❌     |
| **recipe (рецептура)**             | ✅     | ❌        | ✅          | ❌     |
| **packagesCount, volume**          | ✅     | ✅        | ✅          | ✅     |
| **Контакты других участников**     | ❌     | ❌        | ❌          | ❌     |

## 📊 РАСЧЕТ СТОИМОСТЕЙ ПО РОЛЯМ

### ДЛЯ SELLER (полная стоимость):

```typescript
totalAmountForSeller =
  productPrice + // Закупка у поставщика
  fulfillmentServicePrice + // Услуги ФФ
  logisticsPrice + // Доставка
  fulfillmentConsumablesPrice + // Расходники ФФ
  sellerConsumablesPrice // Свои расходники (price × quantity)
```

### ДЛЯ FULFILLMENT (без закупочных цен):

```typescript
totalAmountForFulfillment =
  fulfillmentServicePrice + // Свои услуги
  logisticsPrice + // Доставка (для планирования)
  fulfillmentConsumablesPrice // Свои расходники
// НЕ ВИДИТ: productPrice, sellerConsumablesPrice
```

### ДЛЯ WHOLESALE (только свои товары):

```typescript
totalAmountForWholesale =
  productPrice × quantity           // Только стоимость своих товаров
  // НЕ ВИДИТ: услуги ФФ, логистику, рецептуру
```

### ДЛЯ LOGIST (только доставка):

```typescript
totalAmountForLogist = logisticsPrice // Только стоимость доставки
// НЕ ВИДИТ: цены товаров, услуги, рецептуру
```

## 🛡️ РЕАЛИЗАЦИЯ БЕЗОПАСНОСТИ

### 1. ФИЛЬТРАЦИЯ НА УРОВНЕ RESOLVER

```typescript
// src/graphql/security/supply-data-filter.ts

export class SupplyDataFilter {
  /**
   * Фильтрует данные поставки в зависимости от роли пользователя
   */
  static filterSupplyOrderByRole(order: SupplyOrder, userRole: OrganizationType, userId: string): FilteredSupplyOrder {
    switch (userRole) {
      case 'SELLER':
        return this.filterForSeller(order, userId)

      case 'WHOLESALE':
        return this.filterForWholesale(order, userId)

      case 'FULFILLMENT':
        return this.filterForFulfillment(order, userId)

      case 'LOGIST':
        return this.filterForLogist(order, userId)

      default:
        throw new GraphQLError('Unauthorized organization type')
    }
  }

  /**
   * SELLER видит всю информацию по своим поставкам
   */
  private static filterForSeller(order: SupplyOrder, userId: string): FilteredSupplyOrder {
    // Проверка, что это поставка данного селлера
    if (order.organizationId !== userId) {
      throw new GraphQLError('Access denied to this supply order')
    }

    return {
      ...order,
      // Селлер видит все данные своей поставки
    }
  }

  /**
   * WHOLESALE видит только свои товары без рецептуры
   */
  private static filterForWholesale(order: SupplyOrder, userId: string): FilteredSupplyOrder {
    // Фильтруем только позиции данного поставщика
    const myItems = order.items.filter((item) => item.product.organizationId === userId)

    if (myItems.length === 0) {
      throw new GraphQLError('No items from your organization in this order')
    }

    return {
      ...order,
      items: myItems.map((item) => ({
        ...item,
        // Убираем рецептуру
        recipe: null,
        services: [],
        fulfillmentConsumables: [],
        sellerConsumables: [],
      })),
      // Скрываем общие суммы и услуги
      totalAmount: null,
      fulfillmentServicePrice: null,
      logisticsPrice: null,
      // Оставляем информацию об упаковке
      packagesCount: order.packagesCount,
      volume: order.volume,
    }
  }

  /**
   * FULFILLMENT видит рецептуру, но не видит закупочные цены
   */
  private static filterForFulfillment(order: SupplyOrder, userId: string): FilteredSupplyOrder {
    // Проверка, что поставка для данного ФФ
    if (order.fulfillmentCenterId !== userId) {
      throw new GraphQLError('Access denied to this supply order')
    }

    return {
      ...order,
      items: order.items.map((item) => ({
        ...item,
        // Скрываем закупочные цены
        price: null,
        productPrice: null,
        // Оставляем рецептуру
        recipe: item.recipe,
        // Для расходников селлера показываем только ID и количество
        sellerConsumables: item.sellerConsumables?.map((c) => ({
          id: c.id,
          name: c.name,
          quantity: c.quantity,
          // НЕ показываем цену
        })),
      })),
      // Показываем только свою часть общей суммы
      totalAmount: this.calculateFulfillmentTotal(order),
      productPrice: null, // Скрыто
    }
  }

  /**
   * LOGIST видит только информацию о доставке
   */
  private static filterForLogist(order: SupplyOrder, userId: string): FilteredSupplyOrder {
    // Проверка, что логистика назначена на этот заказ
    if (order.logisticsPartnerId !== userId) {
      throw new GraphQLError('Access denied to this supply order')
    }

    return {
      // Базовая информация
      id: order.id,
      status: order.status,
      deliveryDate: order.deliveryDate,

      // Информация о маршруте
      routes: order.routes.map((route) => ({
        from: route.from,
        fromAddress: route.fromAddress,
        to: route.to,
        toAddress: route.toAddress,
        // Только количество мест и объем
        packagesCount: route.packagesCount,
        volume: route.volume,
      })),

      // Только логистическая информация
      logisticsPrice: order.logisticsPrice,
      totalAmount: order.logisticsPrice, // Только своя сумма

      // Скрываем все остальное
      items: [],
      recipe: null,
      productPrice: null,
      fulfillmentServicePrice: null,
    }
  }

  /**
   * Расчет суммы для фулфилмента
   */
  private static calculateFulfillmentTotal(order: SupplyOrder): number {
    return (
      Number(order.fulfillmentServicePrice || 0) +
      Number(order.logisticsPrice || 0) +
      order.items.reduce((sum, item) => {
        const consumablesPrice =
          item.fulfillmentConsumables?.reduce((cSum, c) => cSum + c.pricePerUnit * c.quantity, 0) || 0
        return sum + consumablesPrice
      }, 0)
    )
  }
}
```

### 2. ИЗОЛЯЦИЯ ДАННЫХ МЕЖДУ УЧАСТНИКАМИ

```typescript
// src/graphql/security/participant-isolation.ts

export class ParticipantIsolation {
  /**
   * Проверяет, что селлеры не видят данные друг друга
   */
  static async validateSellerIsolation(
    prisma: PrismaClient,
    currentUserId: string,
    targetSellerId: string,
  ): Promise<boolean> {
    // Селлер может видеть только свои данные
    if (currentUserId !== targetSellerId) {
      throw new GraphQLError('Access denied to other seller data')
    }
    return true
  }

  /**
   * Проверяет доступ к данным через партнерство
   */
  static async validatePartnerAccess(
    prisma: PrismaClient,
    organizationId: string,
    partnerId: string,
  ): Promise<boolean> {
    const partnership = await prisma.counterparty.findFirst({
      where: {
        OR: [
          {
            organizationId: organizationId,
            counterpartyId: partnerId,
            status: 'ACCEPTED',
          },
          {
            organizationId: partnerId,
            counterpartyId: organizationId,
            status: 'ACCEPTED',
          },
        ],
      },
    })

    if (!partnership) {
      throw new GraphQLError('No active partnership found')
    }

    return true
  }

  /**
   * Группировка заказов для логистики с изоляцией селлеров
   */
  static groupOrdersForLogistics(orders: SupplyOrder[]): GroupedLogisticsOrder[] {
    // Группируем по маршрутам, скрывая информацию о селлерах
    const grouped = orders.reduce(
      (acc, order) => {
        const routeKey = `${order.route.from}-${order.route.to}`

        if (!acc[routeKey]) {
          acc[routeKey] = {
            route: {
              from: order.route.from,
              to: order.route.to,
            },
            orders: [],
            totalPackages: 0,
            totalVolume: 0,
          }
        }

        // Добавляем заказ БЕЗ информации о селлере
        acc[routeKey].orders.push({
          id: order.id,
          packagesCount: order.packagesCount || 0,
          volume: order.volume || 0,
          // НЕ добавляем: organizationId, sellerName и т.д.
        })

        acc[routeKey].totalPackages += order.packagesCount || 0
        acc[routeKey].totalVolume += order.volume || 0

        return acc
      },
      {} as Record<string, GroupedLogisticsOrder>,
    )

    return Object.values(grouped)
  }
}
```

### 3. КОНТРОЛЬ ДОСТУПА К РЕЦЕПТУРЕ

```typescript
// src/graphql/security/recipe-access-control.ts

export class RecipeAccessControl {
  /**
   * Фильтрует рецептуру в зависимости от роли
   */
  static filterRecipeByRole(
    recipe: ProductRecipe,
    userRole: OrganizationType,
    userOrgId: string,
    fulfillmentId?: string,
  ): FilteredRecipe | null {
    switch (userRole) {
      case 'SELLER':
        // Селлер видит полную рецептуру
        return recipe

      case 'FULFILLMENT':
        // ФФ видит рецептуру только если это его заказ
        if (fulfillmentId === userOrgId) {
          return {
            services: recipe.services,
            fulfillmentConsumables: recipe.fulfillmentConsumables.map((c) => ({
              ...c,
              // Показываем pricePerUnit для расчета, НЕ закупочную цену
              price: undefined,
              pricePerUnit: c.pricePerUnit,
            })),
            sellerConsumables: recipe.sellerConsumables.map((c) => ({
              id: c.id,
              name: c.name,
              quantity: c.quantity,
              // НЕ показываем цены расходников селлера
            })),
          }
        }
        return null

      case 'WHOLESALE':
      case 'LOGIST':
        // Поставщик и логистика НЕ видят рецептуру
        return null

      default:
        return null
    }
  }

  /**
   * Проверяет доступ к услугам фулфилмента
   */
  static async validateServiceAccess(
    prisma: PrismaClient,
    serviceIds: string[],
    fulfillmentId: string,
  ): Promise<boolean> {
    const services = await prisma.service.findMany({
      where: {
        id: { in: serviceIds },
        organizationId: fulfillmentId,
      },
    })

    if (services.length !== serviceIds.length) {
      throw new GraphQLError('Some services do not belong to this fulfillment center')
    }

    return true
  }
}
```

### 4. АУДИТ ДОСТУПА К КОММЕРЧЕСКИМ ДАННЫМ

```typescript
// src/graphql/security/commercial-data-audit.ts

export class CommercialDataAudit {
  /**
   * Логирует доступ к коммерческим данным
   */
  static async logAccess(params: {
    userId: string
    organizationType: OrganizationType
    accessType: 'VIEW_PRICE' | 'VIEW_RECIPE' | 'VIEW_CONTACTS'
    resourceType: 'SUPPLY_ORDER' | 'PRODUCT' | 'SERVICE'
    resourceId: string
    metadata?: Record<string, any>
  }): Promise<void> {
    const { userId, organizationType, accessType, resourceType, resourceId, metadata } = params

    // Критические типы доступа требующие особого внимания
    const criticalAccess = [
      'VIEW_PRICE', // Просмотр коммерческих цен
      'VIEW_RECIPE', // Просмотр производственных секретов
    ]

    if (criticalAccess.includes(accessType)) {
      console.warn(
        `🔐 CRITICAL DATA ACCESS: User ${userId} (${organizationType}) accessed ${accessType} for ${resourceType} ${resourceId}`,
      )
    }

    // Сохраняем в базу данных
    await prisma.auditLog.create({
      data: {
        userId,
        organizationType,
        action: `DATA_ACCESS:${accessType}`,
        resourceType,
        resourceId,
        metadata: metadata || {},
        ipAddress: metadata?.ipAddress,
        userAgent: metadata?.userAgent,
        timestamp: new Date(),
      },
    })

    // Проверка на подозрительную активность
    await this.checkSuspiciousActivity(userId, accessType)
  }

  /**
   * Проверка подозрительной активности
   */
  private static async checkSuspiciousActivity(userId: string, accessType: string): Promise<void> {
    // Считаем количество обращений за последний час
    const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000)

    const accessCount = await prisma.auditLog.count({
      where: {
        userId,
        action: { contains: accessType },
        timestamp: { gte: oneHourAgo },
      },
    })

    // Пороги для разных типов доступа
    const thresholds = {
      VIEW_PRICE: 100, // Максимум 100 просмотров цен в час
      VIEW_RECIPE: 50, // Максимум 50 просмотров рецептур в час
      VIEW_CONTACTS: 200, // Максимум 200 просмотров контактов в час
    }

    if (accessCount > thresholds[accessType]) {
      // Отправляем алерт администраторам
      await this.sendSecurityAlert({
        userId,
        type: 'EXCESSIVE_DATA_ACCESS',
        message: `User ${userId} exceeded ${accessType} threshold: ${accessCount} accesses in 1 hour`,
        severity: 'HIGH',
      })
    }
  }

  /**
   * Отправка алертов безопасности
   */
  private static async sendSecurityAlert(alert: {
    userId: string
    type: string
    message: string
    severity: 'LOW' | 'MEDIUM' | 'HIGH'
  }): Promise<void> {
    console.error(`🚨 SECURITY ALERT [${alert.severity}]: ${alert.message}`)

    // TODO: Интеграция с системой алертов (email, SMS, Slack)
    // await notificationService.sendAlert(alert)
  }
}
```

## 🔒 ПРАКТИЧЕСКИЕ ПРИМЕРЫ

### ПРИМЕР 1: Селлер создает поставку товаров

```typescript
// Селлер видит полную информацию
{
  "id": "supply-001",
  "status": "PENDING",
  "items": [{
    "product": { "name": "Товар A", "price": 1000 }, // ✅ Видит закупочную цену
    "quantity": 10,
    "recipe": { // ✅ Видит рецептуру
      "services": ["Упаковка", "Маркировка"],
      "fulfillmentConsumables": ["Пленка", "Скотч"],
      "sellerConsumables": ["Этикетка бренда"]
    }
  }],
  "totalAmount": 15000, // ✅ Видит полную сумму
  "productPrice": 10000,
  "fulfillmentServicePrice": 3000,
  "logisticsPrice": 2000
}
```

### ПРИМЕР 2: Поставщик видит тот же заказ

```typescript
// Поставщик видит только свою часть
{
  "id": "supply-001",
  "status": "PENDING",
  "deliveryDate": "2024-01-15",
  "items": [{
    "product": { "name": "Товар A", "price": 1000 }, // ✅ Видит свою цену
    "quantity": 10
    // ❌ НЕ видит recipe
  }],
  "packagesCount": 2, // ✅ Видит параметры поставки
  "volume": 0.5,
  // ❌ НЕ видит totalAmount, услуги ФФ, логистику
}
```

### ПРИМЕР 3: Фулфилмент видит тот же заказ

```typescript
// Фулфилмент видит рецептуру без закупочных цен
{
  "id": "supply-001",
  "status": "PENDING",
  "items": [{
    "product": { "name": "Товар A" }, // ❌ НЕ видит закупочную цену
    "quantity": 10,
    "recipe": { // ✅ Видит рецептуру
      "services": ["Упаковка", "Маркировка"],
      "fulfillmentConsumables": [{
        "name": "Пленка",
        "pricePerUnit": 50 // ✅ Видит свою цену расходника
      }],
      "sellerConsumables": [{
        "name": "Этикетка бренда",
        "quantity": 10
        // ❌ НЕ видит цену расходников селлера
      }]
    }
  }],
  "totalAmount": 5000, // ✅ Только сумма услуг ФФ + логистика + расходники ФФ
  "fulfillmentServicePrice": 3000,
  "logisticsPrice": 2000
}
```

### ПРИМЕР 4: Логистика видит только доставку

```typescript
// Логистика видит минимум информации
{
  "id": "supply-001",
  "status": "LOGISTICS_CONFIRMED",
  "routes": [{
    "from": "Склад поставщика",
    "fromAddress": "ул. Садовая, 1",
    "to": "Фулфилмент центр",
    "toAddress": "ул. Складская, 10",
    "packagesCount": 2, // ✅ Видит количество мест
    "volume": 0.5 // ✅ Видит объем
  }],
  "logisticsPrice": 2000, // ✅ Видит только свою стоимость
  // ❌ НЕ видит товары, цены, рецептуру, участников
}
```

## ⚠️ КРИТИЧЕСКИЕ ПРАВИЛА БЕЗОПАСНОСТИ

### 1. НИКОГДА НЕ ПОКАЗЫВАТЬ:

- **Фулфилменту** - закупочные цены поставщика (`productPrice`)
- **Поставщику** - рецептуру и услуги фулфилмента
- **Логистике** - коммерческую информацию и рецептуру
- **Селлерам** - данные других селлеров

### 2. ВСЕГДА ПРОВЕРЯТЬ:

- Партнерские отношения перед доступом к данным
- Принадлежность заказа текущей организации
- Роль пользователя перед фильтрацией данных
- Подозрительную активность в логах

### 3. ОБЯЗАТЕЛЬНО ЛОГИРОВАТЬ:

- Все обращения к коммерческим данным
- Попытки несанкционированного доступа
- Массовые запросы данных
- Изменения критических полей

## 🛠️ IMPLEMENTATION CHECKLIST

- [ ] Реализовать `SupplyDataFilter` класс для фильтрации по ролям
- [ ] Добавить `ParticipantIsolation` для изоляции участников
- [ ] Внедрить `RecipeAccessControl` для контроля рецептур
- [ ] Настроить `CommercialDataAudit` для аудита
- [ ] Обновить GraphQL резолверы с новыми фильтрами
- [ ] Добавить тесты безопасности для каждой роли
- [ ] Настроить мониторинг и алерты
- [ ] Провести security review кода

## 📚 СВЯЗАННЫЕ ДОКУМЕНТЫ

- [SECURITY_PRACTICES.md](../infrastructure/SECURITY_PRACTICES.md) - Общие практики безопасности
- [SUPPLY_CHAIN_WORKFLOW.md](./SUPPLY_CHAIN_WORKFLOW.md) - Workflow поставок
- [GRAPHQL_SCHEMA_RULES.md](../api-layer/GRAPHQL_SCHEMA_RULES.md) - Правила GraphQL API

---

_Дата создания: 2025-08-22_  
_Автор: Claude (Anthropic)_  
_Критически важный документ для безопасности коммерческих данных_
