# СИСТЕМА ПАРТНЕРСТВА

## 📋 ОБЗОР

Система партнерства в SFERA реализует механизм установления деловых отношений между различными типами организаций через систему запросов и автоматическую интеграцию после принятия.

## 🔧 АРХИТЕКТУРА СИСТЕМЫ

### Сущности партнерства

```typescript
// Запрос на партнерство (Prisma модель)
model CounterpartyRequest {
  id          String   @id @default(cuid())
  fromId      String   // Кто отправляет запрос
  toId        String   // Кому отправляется запрос
  status      RequestStatus
  message     String?  // Сообщение к запросу
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  from        Organization @relation("RequestFrom", fields: [fromId], references: [id])
  to          Organization @relation("RequestTo", fields: [toId], references: [id])
}

enum RequestStatus {
  PENDING   // Ожидает ответа
  ACCEPTED  // Принят
  REJECTED  // Отклонен
  CANCELLED // Отменен отправителем
}
```

## 🎯 ЖИЗНЕННЫЙ ЦИКЛ ЗАПРОСА ПАРТНЕРСТВА

### 1. Отправка запроса

```typescript
// Мутация: sendCounterpartyRequest
const sendCounterpartyRequest = async (parent, { counterpartyId, message }, { user, prisma }) => {
  // 1. Проверяем существование получателя
  const targetOrganization = await prisma.organization.findUnique({
    where: { id: counterpartyId },
  })

  if (!targetOrganization) {
    throw new Error('Организация не найдена')
  }

  // 2. Проверяем, что не отправляем запрос самому себе
  if (user.organizationId === counterpartyId) {
    throw new Error('Нельзя отправить запрос самому себе')
  }

  // 3. Проверяем существующие запросы
  const existingRequest = await prisma.counterpartyRequest.findFirst({
    where: {
      OR: [
        { fromId: user.organizationId, toId: counterpartyId },
        { fromId: counterpartyId, toId: user.organizationId },
      ],
      status: { in: ['PENDING', 'ACCEPTED'] },
    },
  })

  if (existingRequest) {
    if (existingRequest.status === 'ACCEPTED') {
      throw new Error('Партнерство уже установлено')
    } else {
      throw new Error('Запрос уже отправлен')
    }
  }

  // 4. Создаем новый запрос
  return await prisma.counterpartyRequest.create({
    data: {
      fromId: user.organizationId,
      toId: counterpartyId,
      status: 'PENDING',
      message: message || null,
    },
    include: {
      from: true,
      to: true,
    },
  })
}
```

### 2. Обработка запроса

```typescript
// Мутация: respondToCounterpartyRequest
const respondToCounterpartyRequest = async (parent, { requestId, accept }, { user, prisma }) => {
  const request = await prisma.counterpartyRequest.findUnique({
    where: { id: requestId },
    include: { from: true, to: true },
  })

  if (!request) {
    throw new Error('Запрос не найден')
  }

  // Проверяем права на ответ
  if (request.toId !== user.organizationId) {
    throw new Error('Нет прав для ответа на этот запрос')
  }

  if (request.status !== 'PENDING') {
    throw new Error('Запрос уже обработан')
  }

  const newStatus = accept ? 'ACCEPTED' : 'REJECTED'

  // Обновляем статус запроса
  const updatedRequest = await prisma.counterpartyRequest.update({
    where: { id: requestId },
    data: { status: newStatus },
    include: { from: true, to: true },
  })

  // Если принят - устанавливаем партнерство
  if (accept) {
    await establishPartnership(request.from, request.to, prisma)
  }

  return updatedRequest
}
```

### 3. Отмена запроса

```typescript
// Мутация: cancelCounterpartyRequest
const cancelCounterpartyRequest = async (parent, { requestId }, { user, prisma }) => {
  const request = await prisma.counterpartyRequest.findUnique({
    where: { id: requestId },
  })

  if (!request) {
    throw new Error('Запрос не найден')
  }

  // Только отправитель может отменить
  if (request.fromId !== user.organizationId) {
    throw new Error('Нет прав для отмены запроса')
  }

  if (request.status !== 'PENDING') {
    throw new Error('Можно отменить только ожидающие запросы')
  }

  return await prisma.counterpartyRequest.update({
    where: { id: requestId },
    data: { status: 'CANCELLED' },
  })
}
```

## 🤝 УСТАНОВЛЕНИЕ ПАРТНЕРСТВА

### Автоматическое создание связей

```typescript
const establishPartnership = async (org1, org2, prisma) => {
  // Создаем взаимные связи партнерства
  await prisma.organizationPartner.createMany({
    data: [
      {
        organizationId: org1.id,
        partnerId: org2.id,
      },
      {
        organizationId: org2.id,
        partnerId: org1.id,
      },
    ],
  })

  // Специальная логика для FULFILLMENT + SELLER
  if (shouldCreateWarehouseEntry(org1, org2)) {
    const [fulfillment, seller] = identifyRoles(org1, org2)
    await createWarehouseEntry(seller, fulfillment, prisma)
  }
}

const shouldCreateWarehouseEntry = (org1, org2) => {
  const types = [org1.type, org2.type].sort()
  return types[0] === 'FULFILLMENT' && types[1] === 'SELLER'
}

const identifyRoles = (org1, org2) => {
  if (org1.type === 'FULFILLMENT') return [org1, org2]
  return [org2, org1]
}
```

### Создание записи склада

```typescript
const createWarehouseEntry = async (seller, fulfillment, prisma) => {
  // Извлекаем название магазина из ИП формата
  let storeName = seller.name
  if (seller.fullName && seller.name?.includes('ИП')) {
    const match = seller.fullName.match(/\(([^)]+)\)/)
    if (match && match[1]) {
      storeName = match[1]
    }
  }

  const warehouseEntry = {
    id: `warehouse_${seller.id}_${Date.now()}`,
    storeName: storeName || seller.fullName || seller.name,
    storeOwner: seller.inn || seller.fullName || seller.name,
    storeImage: seller.logoUrl || null,
    storeQuantity: 0,
    partnershipDate: new Date(),
    products: [],
  }

  // Сохраняем в JSON поле склада фулфилмента
  await prisma.organization.update({
    where: { id: fulfillment.id },
    data: {
      warehouseData: {
        ...fulfillment.warehouseData,
        stores: [...(fulfillment.warehouseData?.stores || []), warehouseEntry],
      },
    },
  })
}
```

## 🎁 РЕФЕРАЛЬНАЯ СИСТЕМА

### Генерация реферального кода

```typescript
const generateReferralCode = (organizationName, organizationId) => {
  // Берем первые 3 буквы названия (только кириллица/латиница)
  const cleanName = organizationName.replace(/[^а-яё\w]/gi, '')
  const prefix = cleanName.substring(0, 3).toUpperCase()

  // Добавляем последние 4 символа ID
  const suffix = organizationId.slice(-4).toUpperCase()

  return `${prefix}${suffix}`
}
```

### Автопартнерство по реферальным кодам

```typescript
// При регистрации через реферальный код
const handleReferralRegistration = async (newOrganization, referralCode, prisma) => {
  if (!referralCode) return

  // Находим организацию по реферальному коду
  const referrer = await findByReferralCode(referralCode, prisma)
  if (!referrer) return

  // Автоматически устанавливаем партнерство
  await establishPartnership(newOrganization, referrer, prisma)

  // Создаем транзакцию AUTO_PARTNERSHIP
  await prisma.transaction.create({
    data: {
      id: `txn_auto_partnership_${Date.now()}`,
      organizationId: referrer.id,
      type: 'AUTO_PARTNERSHIP',
      amount: 100, // Бонус за привлечение партнера
      description: `Автопартнерство с ${newOrganization.name}`,
      relatedEntityId: newOrganization.id,
      status: 'COMPLETED',
      createdAt: new Date(),
      balanceAfter: referrer.balance + 100,
    },
  })

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

## 🔍 ЗАПРОСЫ И ФИЛЬТРАЦИЯ

### Получение запросов партнерства

```typescript
// Query: counterpartyRequests
const counterpartyRequests = async (parent, args, { user, prisma }) => {
  const { type = 'received', status } = args

  const where = {
    [type === 'sent' ? 'fromId' : 'toId']: user.organizationId,
  }

  if (status) {
    where.status = status
  }

  return await prisma.counterpartyRequest.findMany({
    where,
    include: {
      from: true,
      to: true,
    },
    orderBy: { createdAt: 'desc' },
  })
}
```

### Поиск потенциальных партнеров

```typescript
const searchOrganizations = async (parent, { query, type, page = 1, limit = 20 }, { user, prisma }) => {
  // Исключаем свою организацию и уже существующих партнеров
  const excludeIds = [user.organizationId]

  const existingPartners = await prisma.organizationPartner.findMany({
    where: { organizationId: user.organizationId },
    select: { partnerId: true },
  })

  excludeIds.push(...existingPartners.map((p) => p.partnerId))

  const where = {
    id: { notIn: excludeIds },
    OR: [
      { name: { contains: query, mode: 'insensitive' } },
      { fullName: { contains: query, mode: 'insensitive' } },
      { inn: { contains: query, mode: 'insensitive' } },
    ],
  }

  if (type) {
    where.type = type
  }

  return await prisma.organization.findMany({
    where,
    skip: (page - 1) * limit,
    take: limit,
    orderBy: { createdAt: 'desc' },
  })
}
```

## 📊 СТАТИСТИКА ПАРТНЕРСТВА

### Счетчики для UI

```typescript
const getPartnershipStats = async (organizationId, prisma) => {
  // Количество активных партнеров
  const partnersCount = await prisma.organizationPartner.count({
    where: { organizationId },
  })

  // Входящие запросы на рассмотрении
  const pendingRequests = await prisma.counterpartyRequest.count({
    where: {
      toId: organizationId,
      status: 'PENDING',
    },
  })

  // Отправленные запросы в ожидании
  const sentRequests = await prisma.counterpartyRequest.count({
    where: {
      fromId: organizationId,
      status: 'PENDING',
    },
  })

  return {
    partnersCount,
    pendingRequests,
    sentRequests,
  }
}
```

## 🎨 ИНТЕГРАЦИЯ С UI

### Уведомления в реальном времени

```typescript
// Подписка на изменения запросов партнерства
const counterpartyRequestUpdated = {
  subscribe: withFilter(
    () => pubsub.asyncIterator('COUNTERPARTY_REQUEST_UPDATED'),
    (payload, variables, context) => {
      // Уведомляем только заинтересованные организации
      return (
        payload.counterpartyRequestUpdated.toId === context.user.organizationId ||
        payload.counterpartyRequestUpdated.fromId === context.user.organizationId
      )
    },
  ),
}
```

### Компонент управления партнерством

```typescript
// Пример использования в React компоненте
const PartnershipManager = () => {
  const { data: requests } = useQuery(GET_COUNTERPARTY_REQUESTS)
  const [sendRequest] = useMutation(SEND_COUNTERPARTY_REQUEST)
  const [respondToRequest] = useMutation(RESPOND_TO_COUNTERPARTY_REQUEST)

  // Логика отправки запроса
  const handleSendRequest = async (partnerId: string, message?: string) => {
    await sendRequest({
      variables: { counterpartyId: partnerId, message },
    })
  }

  // Логика ответа на запрос
  const handleResponse = async (requestId: string, accept: boolean) => {
    await respondToRequest({
      variables: { requestId, accept },
    })
  }
}
```

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

### Проверки доступа

1. **Отправка запроса**: только аутентифицированные пользователи
2. **Ответ на запрос**: только получатель может ответить
3. **Отмена запроса**: только отправитель может отменить
4. **Предотвращение дублирования**: проверка существующих запросов
5. **Самоисключение**: нельзя отправить запрос самому себе

### Валидация данных

1. **Существование организации**: проверка перед отправкой запроса
2. **Статус запроса**: можно отвечать только на PENDING запросы
3. **Права доступа**: проверка принадлежности к организации
4. **Целостность данных**: атомарные операции при установлении партнерства

## 📈 МЕТРИКИ И АНАЛИТИКА

### Ключевые показатели

- **Коэффициент принятия**: процент принятых запросов
- **Время ответа**: среднее время обработки запросов
- **Активность партнерства**: количество операций между партнерами
- **Эффективность рефералов**: процент автопартнерств от общего числа

### Отчеты

- **Топ реферальных организаций**: по количеству привлеченных партнеров
- **География партнерства**: распределение по регионам
- **Тренды установления партнерства**: динамика по времени
- **Конверсия запросов**: от отправки до установления связи
