# ЯДРО БИЗНЕС-ПРАВИЛ СИСТЕМЫ SFERA

## 🎯 ОСНОВНЫЕ ПРИНЦИПЫ СИСТЕМЫ

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

**Правило изоляции организаций:**

- ✅ **FULFILLMENT**: Полный доступ к своим операциям и складам
- ✅ **SELLER**: Доступ только к своим данным, НЕТ доступа к чужим данным
- ✅ **WHOLESALE**: Доступ к своим товарам и заказам
- ✅ **LOGIST**: Доступ к назначенным маршрутам доставки

**Правило видимости:**

```typescript
// В resolvers.ts найдено правило:
const hasAccess = organization.users.some((user) => user.id === context.user!.id)
if (!hasAccess) {
  throw new GraphQLError('Нет доступа к этой организации')
}
```

### 2. ПРИНЦИП ПАРТНЕРСТВА

**Система заявок на партнерство:**

- Статусы: `PENDING` → `ACCEPTED` | `REJECTED` | `CANCELLED`
- Автоматическое создание складских записей при принятии партнерства
- Контрагенты видят только товары/услуги партнеров

**Автоматическое партнерство:**

```typescript
// Из кода: автоматическое создание записей склада (реальная реализация)
const autoCreateWarehouseEntry = async (sellerId: string, fulfillmentId: string) => {
  console.warn(`🏗️ AUTO WAREHOUSE ENTRY: Creating for seller ${sellerId} with fulfillment ${fulfillmentId}`)

  // Получаем данные селлера
  const sellerOrg = await prisma.organization.findUnique({
    where: { id: sellerId },
  })

  if (!sellerOrg) {
    throw new Error(`Селлер с ID ${sellerId} не найден`)
  }

  // ЛОГИКА ОПРЕДЕЛЕНИЯ НАЗВАНИЯ МАГАЗИНА
  let storeName = sellerOrg.name

  if (sellerOrg.fullName && sellerOrg.name?.includes('ИП')) {
    // Извлекаем название из скобок: "ИП Антипова Д. В. (Renrel)" -> "Renrel"
    const match = sellerOrg.fullName.match(/\(([^)]+)\)/)
    if (match && match[1]) {
      storeName = match[1]
    }
  }

  // Создаем структуру данных для склада
  const warehouseEntry = {
    id: `warehouse_${sellerId}_${Date.now()}`,
    storeName: storeName || sellerOrg.fullName || sellerOrg.name,
    storeOwner: sellerOrg.inn || sellerOrg.fullName || sellerOrg.name,
    storeImage: sellerOrg.logoUrl || null,
    storeQuantity: 0,
    partnershipDate: new Date(),
    products: [],
  }

  return warehouseEntry
}
```

### 3. ПРИНЦИП ТИПИЗАЦИИ РАСХОДНИКОВ

**Два независимых типа расходников:**

#### FULFILLMENT_CONSUMABLES (Расходники фулфилмента)

- **Назначение**: Операционные нужды фулфилмента
- **Владелец**: Фулфилмент
- **Заказчик**: Фулфилмент заказывает у поставщиков
- **Использование**: Внутренние операции фулфилмента

#### SELLER_CONSUMABLES (Расходники селлеров)

- **Назначение**: Расходники селлеров на хранении
- **Владелец**: Селлер
- **Место хранения**: Склад фулфилмента
- **Использование**: В рецептурах продуктов селлера

## 🔄 WORKFLOW ПРАВИЛА

### СИСТЕМА СТАТУСОВ ПОСТАВОК

**8-статусная система (из GraphQL enum):**

```typescript
enum SupplyOrderStatus {
  PENDING             // Ожидает одобрения поставщика
  SUPPLIER_APPROVED   // Поставщик одобрил, ожидает логистику
  LOGISTICS_CONFIRMED // Логистика подтвердила, ожидает отправки
  SHIPPED             // Отправлено поставщиком, в пути
  DELIVERED           // Доставлено и принято фулфилментом
  CANCELLED           // Отменено (любой участник может отменить)

  // Legacy статусы (для обратной совместимости):
  CONFIRMED           // Устаревший
  IN_TRANSIT          // Устаревший
}
```

**Правила переходов статусов:**

- `PENDING` → `SUPPLIER_APPROVED` (действие поставщика)
- `SUPPLIER_APPROVED` → `LOGISTICS_CONFIRMED` (действие логистики)
- `LOGISTICS_CONFIRMED` → `SHIPPED` (действие поставщика)
- `SHIPPED` → `DELIVERED` (действие фулфилмента)
- Любой статус → `CANCELLED` (любой участник)

### ПРАВИЛА РОЛЕЙ В ПОСТАВКАХ

**Из кода resolvers.ts найдены правила доступа:**

#### Для ПОСТАВЩИКОВ (WHOLESALE):

```typescript
// Входящие заказы для поставщиков - требуют подтверждения
const incomingSupplierOrders = await prisma.supplyOrder.count({
  where: {
    partnerId: currentUser.organization.id, // Мы - поставщик
    status: 'PENDING', // Ожидает подтверждения от поставщика
  },
})
```

#### Для ЛОГИСТИКИ (LOGIST):

```typescript
// Логистические заявки для логистики - требуют действий (реальный код)
const logisticsOrders = await prisma.supplyOrder.count({
  where: {
    logisticsPartnerId: currentUser.organization.id, // Мы - назначенная логистика
    status: {
      in: [
        'CONFIRMED', // Legacy: Подтверждено фулфилментом - нужно подтвердить логистикой
        'SUPPLIER_APPROVED', // Подтверждено поставщиком - нужно подтвердить логистикой
        'LOGISTICS_CONFIRMED', // Подтверждено логистикой - нужно забрать товар у поставщика
      ],
    },
  },
})
```

#### Для ФУЛФИЛМЕНТА:

```typescript
// Фулфилмент получает счетчики по типу организации (реальный код)
if (currentUser.organization.type === 'FULFILLMENT') {
  pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders // Комбинированный счетчик

  // ourSupplyOrders: собственные заказы расходников ФФ
  // sellerSupplyOrders: заказы товаров от селлеров (где ФФ - получатель)
} else if (currentUser.organization.type === 'WHOLESALE') {
  pendingSupplyOrders = incomingSupplierOrders // Входящие заказы для подтверждения
} else if (currentUser.organization.type === 'LOGIST') {
  pendingSupplyOrders = logisticsOrders // Логистические задачи
}
```

## 📊 ПРАВИЛА РЕЦЕПТУР ПРОДУКТОВ

**Структура рецептуры (из GraphQL schema):**

```typescript
type ProductRecipe {
  services: [Service!]!                    // Услуги фулфилмента
  fulfillmentConsumables: [Supply!]!       // Расходники фулфилмента
  sellerConsumables: [Supply!]!            // Расходники селлера
  marketplaceCardId: String                // Связь с карточкой маркетплейса
}
```

**Экономические правила рецептур:**

- Когда селлер выбирает расходники фулфилмента → формируется экономика:
  - В кабинете селлера: расход на расходники фулфилмента
  - В кабинете фулфилмента: доход от продажи расходников селлеру

## 🔐 ПРАВИЛА БЕЗОПАСНОСТИ

### JWT Токены

- Срок действия: 30 дней
- Payload: `{ userId, phone }`
- Обязательная проверка принадлежности к организации

### Валидация доступа к данным

```typescript
// Проверка принадлежности пользователя к организации
const currentUser = await prisma.user.findUnique({
  where: { id: context.user.id },
  include: { organization: true },
})

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

// Проверка доступа к конкретной организации (из реального кода)
const hasAccess = organization.users.some((user) => user.id === context.user!.id)
if (!hasAccess) {
  throw new GraphQLError('Нет доступа к этой организации', {
    extensions: { code: 'FORBIDDEN' },
  })
}
```

### Правила доступа по типам организаций (примеры из кода):

```typescript
// Только фулфилмент может управлять услугами
if (currentUser.organization.type !== 'FULFILLMENT') {
  throw new GraphQLError('Услуги доступны только для фулфилмент центров')
}

// Только поставщики могут управлять каталогом товаров
if (currentUser.organization.type !== 'WHOLESALE') {
  throw new GraphQLError('Товары доступны только для поставщиков')
}

// Фулфилмент имеет доступ к складским операциям
if (currentUser.organization.type !== 'FULFILLMENT') {
  throw new GraphQLError('Товары склада доступны только для фулфилмент центров')
}

// Обновление цен расходников - только для ФФ
if (currentUser.organization.type !== 'FULFILLMENT') {
  throw new GraphQLError('Обновление цен расходников доступно только для фулфилмент центров')
}
```

## 🎛️ ПРАВИЛА ИНТЕГРАЦИЙ С МАРКЕТПЛЕЙСАМИ

### API Ключи

- Поддержка: Wildberries, Ozon
- Валидация при добавлении
- Безопасное хранение в БД

### Кеширование данных

- Складские данные WB: кеш с TTL
- Статистика продаж: кеш по периодам
- Обновление по требованию

## 🔄 ПРАВИЛА РЕФЕРАЛЬНОЙ СИСТЕМЫ

### Генерация реферальных кодов (из реального кода):

```typescript
// Алгоритм генерации уникального реферального кода
const generateReferralCode = async (): Promise<string> => {
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' // Исключены похожие символы
  let attempts = 0
  const maxAttempts = 10

  while (attempts < maxAttempts) {
    let code = ''
    for (let i = 0; i < 10; i++) {
      // 10-символьный код
      code += chars.charAt(Math.floor(Math.random() * chars.length))
    }

    // Проверяем уникальность в БД
    const existing = await prisma.organization.findUnique({
      where: { referralCode: code },
    })

    if (!existing) {
      return code
    }
    attempts++
  }

  // Fallback если не удалось сгенерировать
  return `REF${Date.now()}${Math.random().toString(36).substr(2, 5).toUpperCase()}`
}
```

### Автоматические начисления:

```typescript
// При регистрации по реферальной ссылке (реальный код из resolvers.ts:2930-2965)
if (referralCode) {
  const referrer = await prisma.organization.findUnique({
    where: { referralCode: referralCode },
  })

  if (referrer) {
    // Создаем реферальную транзакцию (100 сфер)
    await prisma.referralTransaction.create({
      data: {
        referrerId: referrer.id,
        referralId: organization.id,
        points: 100,
        type: 'REGISTRATION',
        description: `Регистрация ${type.toLowerCase()} организации по реферальной ссылке`,
      },
    })

    // Увеличиваем счетчик сфер у реферера
    await prisma.organization.update({
      where: { id: referrer.id },
      data: { referralPoints: { increment: 100 } },
    })

    // Устанавливаем связь реферала
    await prisma.organization.update({
      where: { id: organization.id },
      data: { referredById: referrer.id },
    })
  }
}

// Партнерские коды (дополнительная система)
if (partnerCode) {
  const partner = await prisma.organization.findUnique({
    where: { referralCode: partnerCode },
  })

  if (partner) {
    // Создаем партнерскую транзакцию (100 сфер)
    await prisma.referralTransaction.create({
      data: {
        referrerId: partner.id,
        referralId: organization.id,
        points: 100,
        type: 'AUTO_PARTNERSHIP',
        description: `Автопартнерство с ${type.toLowerCase()} организацией`,
      },
    })

    // Обновляем баланс партнера
    await prisma.organization.update({
      where: { id: partner.id },
      data: { referralPoints: { increment: 100 } },
    })
  }
}
```

### Типы начислений (из реальных транзакций):

- `REGISTRATION` - регистрация по реферальной ссылке (100 баллов)
- `AUTO_PARTNERSHIP` - автоматическое деловое партнерство (100 баллов)
- `FIRST_ORDER` - первый заказ реферала
- `MONTHLY_BONUS` - ежемесячные бонусы за активность

## 💰 ПРАВИЛА ЭКОНОМИЧЕСКОЙ МОДЕЛИ

### Система баланса организаций

```typescript
// Структура баланса в Organization model
{
  balance: number,              // Основной баланс в рублях
  referralPoints: number,       // Реферальные баллы ("сферы")
  creditLimit?: number,         // Кредитный лимит
  paymentMethods: Json         // Методы оплаты
}
```

### Автоматические транзакции

```typescript
// Пример создания транзакции с обновлением баланса (из реального кода)
const createBalanceTransaction = async (
  organizationId: string,
  amount: number,
  type: TransactionType,
  description: string,
  relatedEntityId?: string,
) => {
  const org = await prisma.organization.findUnique({
    where: { id: organizationId },
  })

  const newBalance = org.balance + amount

  // Атомарная операция: создание транзакции + обновление баланса
  await prisma.$transaction([
    prisma.transaction.create({
      data: {
        id: `txn_${type.toLowerCase()}_${Date.now()}`,
        organizationId,
        type,
        amount,
        description,
        relatedEntityId,
        status: 'COMPLETED',
        createdAt: new Date(),
        balanceAfter: newBalance,
      },
    }),
    prisma.organization.update({
      where: { id: organizationId },
      data: { balance: newBalance },
    }),
  ])
}
```

## 📋 ПРАВИЛА ВАЛИДАЦИИ ДОСТУПА ПО РОЛЯМ

### Системы проверки прав (расширенные примеры):

```typescript
// 1. Проверка принадлежности к организации (базовая)
const validateUserOrganizationAccess = async (userId: string, organizationId: string) => {
  const organization = await prisma.organization.findUnique({
    where: { id: organizationId },
    include: { users: true },
  })

  const hasAccess = organization.users.some((user) => user.id === userId)
  if (!hasAccess) {
    throw new GraphQLError('Нет доступа к этой организации', {
      extensions: { code: 'FORBIDDEN' },
    })
  }
  return organization
}

// 2. Проверка доступа по типу операции (из реального кода)
const validateOperationAccess = (userOrgType: string, operation: string) => {
  const accessRules = {
    FULFILLMENT: [
      'manage_services', // Управление услугами ФФ
      'manage_consumables', // Управление расходниками ФФ
      'view_warehouse', // Просмотр склада
      'manage_warehouse', // Управление складом
      'receive_orders', // Прием заказов от селлеров
      'update_consumable_prices', // Обновление цен расходников
    ],
    SELLER: [
      'view_own_supplies', // Просмотр своих поставок
      'create_supply_orders', // Создание заказов поставок
      'manage_recipes', // Управление рецептурами продуктов
      'view_partner_services', // Просмотр услуг партнеров-ФФ
    ],
    WHOLESALE: [
      'manage_products', // Управление каталогом товаров
      'approve_orders', // Подтверждение заказов от селлеров
      'update_product_prices', // Обновление цен товаров
      'view_incoming_orders', // Просмотр входящих заказов
    ],
    LOGIST: [
      'view_assigned_routes', // Просмотр назначенных маршрутов
      'confirm_logistics', // Подтверждение логистики
      'update_delivery_status', // Обновление статуса доставки
      'manage_routes', // Управление маршрутами
    ],
  }

  if (!accessRules[userOrgType]?.includes(operation)) {
    throw new GraphQLError(`Операция ${operation} недоступна для ${userOrgType}`)
  }
}

// 3. Проверка доступа к данным партнеров
const validatePartnerAccess = async (userOrgId: string, targetOrgId: string) => {
  const partnership = await prisma.organizationPartner.findFirst({
    where: {
      organizationId: userOrgId,
      partnerId: targetOrgId,
    },
  })

  if (!partnership) {
    throw new GraphQLError('Доступ разрешен только к данным партнеров')
  }
}
```

## 🔄 ПРАВИЛА СТАТУСНЫХ ПЕРЕХОДОВ (ДЕТАЛИЗАЦИЯ)

### Бизнес-логика переходов статусов:

```typescript
// Правила изменения статуса поставки (из реального workflow)
const validateStatusTransition = (
  currentStatus: SupplyOrderStatus,
  newStatus: SupplyOrderStatus,
  userOrgType: string,
  userOrgId: string,
  order: SupplyOrder
) => {
  const allowedTransitions = {
    'PENDING': {
      'SUPPLIER_APPROVED': {
        allowedBy: ['WHOLESALE'],
        condition: (order) => order.partnerId === userOrgId  // Только поставщик-получатель заказа
      },
      'CANCELLED': {
        allowedBy: ['SELLER', 'FULFILLMENT', 'WHOLESALE'], // Любой участник может отменить
        condition: () => true
      }
    },

    'SUPPLIER_APPROVED': {
      'LOGISTICS_CONFIRMED': {
        allowedBy: ['LOGIST'],
        condition: (order) => order.logisticsPartnerId === userOrgId // Только назначенная логистика
      },
      'CANCELLED': {
        allowedBy: ['SELLER', 'FULFILLMENT', 'WHOLESALE', 'LOGIST'],
        condition: () => true
      }
    },

    'LOGISTICS_CONFIRMED': {
      'SHIPPED': {
        allowedBy: ['WHOLESALE'],
        condition: (order) => order.partnerId === userOrgId // Только поставщик отправляет
      },
      'CANCELLED': {
        allowedBy: ['SELLER', 'FULFILLMENT', 'WHOLESALE', 'LOGIST'],
        condition: () => true
      }
    },

    'SHIPPED': {
      'DELIVERED': {
        allowedBy: ['FULFILLMENT'],
        condition: (order) => order.fulfillmentCenterId === userOrgId // Только получающий ФФ
      }
    }
  }

  const transition = allowedTransitions[currentStatus]?.[newStatus]
  if (!transition) {
    throw new GraphQLError(`Переход ${currentStatus} → ${newStatus} недопустим`)
  }

  if (!transition.allowedBy.includes(userOrgType)) {
    throw new GraphQLError(`Организация типа ${userOrgType} не может выполнить переход ${currentStatus} → ${newStatus}`)
  }

  if (!transition.condition(order)) {
    throw new GraphQLError('Условия для перехода статуса не выполнены')
  }
}

---

*Извлечено из анализа: GraphQL resolvers, Prisma models, бизнес-логика*
*Дата: 2025-08-21*
```
