ДЕТАЛИЗАЦИЯ ТЕХНИЧЕСКОГО ЗАДАНИЯ

B2B-платформа инвестиций в бизнес

БЛОК 1: ПОЛЬЗОВАТЕЛЬСКИЕ РОЛИ И ПРАВА ДОСТУПА

1.1. Детализация ролей и сценариев использования

Раздел ТЗ: Роли пользователей

Суть проблемы:
Недостаточно описаны конкретные возможности каждой роли, не определены сценарии переключения между ролями, отсутствует матрица прав доступа.

Предлагаемое решение:

1.1.1. Матрица ролей и прав доступа

Функционал Инвестор (Free) Инвестор (Premium) Предприниматель (Free) Предприниматель (Premium) Администратор Модератор
Просмотр объявлений ✓ (10/день) ✓ (безлимит) ✓ (5/день) ✓ (безлимит)
Создание объявлений ✓ (1 активное) ✓ (5 активных)
Отклик на объявления ✓ (3/месяц) ✓ (безлимит)
Просмотр контактов По запросу ✓ (после одобрения)
Внутренний чат ✓ (базовый) ✓ (расширенный) ✓ (базовый) ✓ (расширенный)
Продвижение объявлений ✓ (платно) ✓ (включено)
Аналитика Базовая Расширенная Базовая Расширенная Полная Частичная
Верификация Опционально Включена Опционально Включена Проверка док-тов
Избранное ✓ (20 макс) ✓ (безлимит) ✓ (20 макс) ✓ (безлимит)
Экспорт данных ✓ (CSV/Excel) ✓ (CSV/Excel)

1.1.2. Механизм переключения между ролями

Бизнес-логика:

  1. Один аккаунт = множественные роли
    • Пользователь может быть одновременно инвестором и предпринимателем
    • Переключение через dropdown в шапке профиля
    • Отдельные балансы, настройки и история для каждой роли
    • Единый email и телефон для всех ролей
  2. Правила переключения:
    IF пользователь имеет обе роли THEN
      - Показывать переключатель ролей в header
      - При переходе в раздел автоматически определять нужную роль
      - Сохранять последнюю активную роль в сессии
    
    IF пользователь хочет добавить роль THEN
      - Кнопка "Добавить роль" в настройках профиля
      - Заполнение профиля для новой роли (отдельная форма)
      - Модерация новой роли (если требуется верификация)
    
  3. UI/UX переключения:
    • Визуальная индикация активной роли (цветовая схема: синий = инвестор, зеленый = предприниматель)
    • Уведомление при первом переключении с подсказками
    • Быстрый доступ к функциям другой роли через контекстное меню

Технические требования:

// Структура данных пользователя
{
  userId: "UUID",
  email: "user@example.com",
  roles: [
    {
      roleType: "investor",
      tier: "premium",
      profileId: "INV-UUID",
      isActive: true,
      settings: {...},
      balance: 0,
      statistics: {...}
    },
    {
      roleType: "entrepreneur",
      tier: "free",
      profileId: "ENT-UUID",
      isActive: false,
      settings: {...},
      balance: 0,
      statistics: {...}
    }
  ],
  currentRole: "investor",
  lastRoleSwitch: "2024-01-15T10:30:00Z"
}

Примеры use-cases:

UC-1.1: Инвестор хочет стать предпринимателем

1. Пользователь заходит в "Настройки профиля"
2. Нажимает "Добавить роль предпринимателя"
3. Заполняет форму с данными компании (ИНН, ОГРН, описание)
4. Загружает документы (устав, выписка из ЕГРЮЛ)
5. Отправляет на модерацию
6. Получает уведомление о результате проверки (24-48 часов)
7. После одобрения видит переключатель ролей в header

UC-1.2: Переключение между ролями

1. Пользователь кликает на dropdown с текущей ролью в header
2. Видит список доступных ролей с иконками
3. Выбирает нужную роль
4. Страница обновляется с контентом для выбранной роли
5. Навигация и доступные функции меняются соответственно

Приоритет: [CRITICAL] — MVP

Обоснование решения:

  • Гибкость: Пользователь может быть и инвестором, и предпринимателем, что увеличивает engagement
  • Монетизация: Возможность продавать Premium для каждой роли отдельно
  • Безопасность: Разделение данных по ролям предотвращает конфликты интересов
  • UX: Единый аккаунт удобнее, чем создание нескольких профилей

Риски, которые закрывает:

  • Конфликт интересов (пользователь видит данные в разных контекстах)
  • Путаница в интерфейсе (четкое разделение функционала)
  • Проблемы с биллингом (отдельные подписки для каждой роли)

Влияние на метрики:

  • Увеличение LTV на 40-60% (пользователь платит за обе роли)
  • Снижение CAC (один пользователь = два сегмента)
  • Рост engagement на 30-40%

Связанные изменения:

  • Раздел «Биллинг и оплата» — добавить управление подписками по ролям
  • Раздел «Уведомления» — разделить настройки по ролям
  • Раздел «Профиль» — создать вкладки для каждой роли

1.2. Детализация тарифных планов

Раздел ТЗ: Монетизация

Суть проблемы:
Не определены конкретные лимиты, не описаны механизмы ограничений, отсутствует логика перехода между тарифами.

Предлагаемое решение:

1.2.1. Тарифные планы для инвесторов

Параметр Free Premium (₽4,990/мес) Enterprise (по запросу)
Просмотр объявлений 10/день Безлимит Безлимит
Откликов на объявления 3/месяц Безлимит Безлимит
Доступ к контактам ✓ + приоритет
Сохраненные объявления 20 Безлимит Безлимит
Расширенный поиск Базовый ✓ + AI-рекомендации
Аналитика рынка Базовая Расширенная + отчеты
Верификация профиля Опционально (₽2,000) Включена Включена
Приоритет в откликах ✓ + выделение
Экспорт данных CSV CSV + API
Техподдержка Email (48ч) Email (24ч) + чат Персональный менеджер
Скрытие от конкурентов
Ранний доступ к объявлениям +24 часа +48 часов

1.2.2. Тарифные планы для предпринимателей

Параметр Free Premium (₽7,990/мес) Premium+ (₽14,990/мес)
Активных объявлений 1 5 15
Продвижение объявлений ₽500/неделя 2 бесплатно/мес 5 бесплатно/мес
Позиция в выдаче Стандартная Топ-10 Топ-3
Аналитика объявлений Базовая Расширенная Полная + A/B тесты
Верификация компании Опционально (₽5,000) Включена Включена + бейдж
Ответов на отклики 10/месяц Безлимит Безлимит
Шаблоны ответов 5 20
Автоответчик
CRM-интеграция
Мультиаккаунт (сотрудники) 2 пользователя 10 пользователей
Брендирование профиля Логотип Полное оформление
Приоритетная модерация ✓ (4 часа) ✓ (2 часа)

1.2.3. Бизнес-логика ограничений

Механизм подсчета лимитов:

# Псевдокод системы лимитов
class RateLimiter:
    def check_limit(user_id, action_type, role):
        # Получаем тарифный план пользователя
        plan = get_user_plan(user_id, role)
        
        # Проверяем лимит для действия
        limit = LIMITS[plan][action_type]
        current_usage = get_usage(user_id, action_type, period)
        
        if limit == "unlimited":
            return True
        
        if current_usage >= limit:
            return False, {
                "error": "limit_reached",
                "current": current_usage,
                "max": limit,
                "reset_at": get_reset_time(period),
                "upgrade_url": "/pricing"
            }
        
        return True
    
    def increment_usage(user_id, action_type):
        # Увеличиваем счетчик использования
        redis.incr(f"usage:{user_id}:{action_type}:{period}")
        redis.expire(f"usage:{user_id}:{action_type}:{period}", TTL)

Правила сброса лимитов:

  1. Ежедневные лимиты (просмотры объявлений):
    • Сброс в 00:00 по московскому времени
    • Уведомление за 2 часа до сброса, если лимит исчерпан
  2. Ежемесячные лимиты (отклики, объявления):
    • Сброс в день оплаты подписки (billing cycle)
    • Уведомление за 3 дня до сброса с статистикой использования
  3. Одновременные лимиты (активные объявления):
    • Проверка при создании нового объявления
    • Возможность архивировать старое для публикации нового

Поведение при достижении лимита:

IF пользователь достиг лимита THEN
  1. Показать модальное окно с информацией:
     - "Вы использовали X из Y доступных [действий]"
     - Дата следующего сброса лимита
     - Кнопка "Перейти на Premium" (с указанием выгод)
     - Кнопка "Напомнить позже"
  
  2. Отправить email с предложением апгрейда:
     - Персонализированное предложение
     - Скидка 20% при оплате в течение 24 часов
     - Калькулятор выгоды от Premium
  
  3. Логировать событие для аналитики:
     - Тип лимита
     - Частота достижения
     - Конверсия в upgrade

1.2.4. Механизм перехода между тарифами

Апгрейд (Free → Premium):

1. Пользователь нажимает "Перейти на Premium"
2. Показывается страница с выбором тарифа:
   - Помесячная оплата (полная стоимость)
   - Годовая оплата (скидка 20%)
   - Квартальная оплата (скидка 10%)
3. Выбор способа оплаты:
   - Банковская карта (Stripe/CloudPayments)
   - Счет для юрлиц (выставление счета)
   - СБП (для РФ)
4. Оплата
5. Мгновенная активация Premium
6. Email-подтверждение с чеком и деталями подписки
7. Onboarding для новых функций Premium

Даунгрейд (Premium → Free):

1. Пользователь отменяет подписку в настройках
2. Показывается опрос:
   - Причина отмены (обязательно)
   - Предложение скидки 30% на следующий месяц
   - Предложение паузы подписки на 1 месяц
3. Если пользователь подтверждает отмену:
   - Premium действует до конца оплаченного периода
   - За 7 дней до окончания - напоминание с предложением продлить
   - За 1 день до окончания - финальное напоминание
   - После окончания - переход на Free с сохранением данных
4. Ограничения после даунгрейда:
   - Лишние объявления переходят в архив (можно выбрать, какие оставить)
   - История откликов сохраняется, но доступна только для чтения
   - Аналитика доступна только за последние 30 дней

Технические требования:

-- Таблица подписок
CREATE TABLE subscriptions (
    id UUID PRIMARY KEY,
    user_id UUID REFERENCES users(id),
    role_type VARCHAR(20), -- 'investor' или 'entrepreneur'
    plan_type VARCHAR(20), -- 'free', 'premium', 'enterprise'
    status VARCHAR(20), -- 'active', 'cancelled', 'expired', 'paused'
    started_at TIMESTAMP,
    expires_at TIMESTAMP,
    billing_cycle VARCHAR(20), -- 'monthly', 'quarterly', 'yearly'
    amount DECIMAL(10,2),
    currency VARCHAR(3),
    payment_method VARCHAR(50),
    auto_renew BOOLEAN DEFAULT true,
    cancelled_at TIMESTAMP,
    cancellation_reason TEXT,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Таблица использования лимитов
CREATE TABLE usage_limits (
    id UUID PRIMARY KEY,
    user_id UUID REFERENCES users(id),
    role_type VARCHAR(20),
    action_type VARCHAR(50), -- 'view_listing', 'create_response', etc.
    period_type VARCHAR(20), -- 'daily', 'monthly'
    period_start TIMESTAMP,
    period_end TIMESTAMP,
    usage_count INTEGER DEFAULT 0,
    limit_value INTEGER,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

Примеры use-cases:

UC-1.3: Инвестор исчерпал дневной лимит просмотров

1. Пользователь просмотрел 10 объявлений (лимит Free-плана)
2. При попытке открыть 11-е объявление видит модальное окно:
   "Вы достигли дневного лимита просмотров (10/10)
   Лимит обновится через 8 часов 23 минуты
   
   [Перейти на Premium] - безлимитный просмотр за ₽4,990/мес
   [Напомнить завтра]"
3. Если выбирает Premium:
   - Переход на страницу оплаты
   - После оплаты мгновенный доступ к просмотру
4. Если выбирает "Напомнить":
   - Push-уведомление в момент сброса лимита
   - Email с подборкой новых объявлений

UC-1.4: Предприниматель хочет опубликовать второе объявление на Free

1. Пользователь нажимает "Создать объявление"
2. Система проверяет: уже есть 1 активное объявление (лимит Free)
3. Показывается модальное окно:
   "У вас уже есть активное объявление
   
   Варианты:
   [Архивировать старое объявление] - бесплатно
   [Перейти на Premium] - до 5 активных объявлений за ₽7,990/мес
   [Отменить]"
4. Если выбирает архивацию:
   - Показывается список активных объявлений
   - Пользователь выбирает, какое архивировать
   - Новое объявление публикуется
5. Если выбирает Premium:
   - Переход на страницу оплаты
   - После оплаты возврат к созданию объявления

Приоритет: [CRITICAL] — MVP

Обоснование решения:

  • Монетизация: Четкие лимиты стимулируют переход на платные планы
  • Прозрачность: Пользователь всегда знает свой статус и ограничения
  • Гибкость: Возможность выбора между архивацией и апгрейдом
  • Retention: Мягкие ограничения не отталкивают пользователей

Риски, которые закрывает:

  • Злоупотребление бесплатным тарифом
  • Неясность ценности Premium
  • Фрустрация от неожиданных ограничений
  • Потеря revenue из-за нечетких границ тарифов

Влияние на метрики:

  • Конверсия Free → Premium: 8-12% (бенчмарк SaaS)
  • ARPU увеличится на 300-400%
  • Churn rate Premium: <5% при правильной коммуникации
  • Trial-to-paid: 25-30% при наличии триального периода

Связанные изменения:

  • Раздел «Биллинг» — добавить управление подписками
  • Раздел «Уведомления» — добавить алерты о лимитах
  • Раздел «Аналитика» — добавить метрики по использованию лимитов
  • Раздел «Админ-панель» — добавить управление тарифами

БЛОК 2: ВЕРИФИКАЦИЯ И ДОВЕРИЕ

2.1. Процедуры KYC и верификации

Раздел ТЗ: Верификация пользователей

Суть проблемы:
Не описан процесс верификации, не определены требуемые документы, отсутствует логика проверки и критерии одобрения.

Предлагаемое решение:

2.1.1. Уровни верификации

Трехуровневая система:

Уровень Требования Доступные возможности Бейдж
Базовый Email + телефон Базовый функционал
Подтвержденный + паспорт/ID Увеличенные лимиты, доверие +30% ✓ Verified
Премиум + финансовые документы + видеозвонок Полный доступ, доверие +70% ⭐ Premium Verified

2.1.2. Процесс верификации для инвесторов

Шаг 1: Email и телефон (обязательно для всех)

Процесс:
1. Регистрация с email
2. Отправка кода подтверждения на email (6 цифр, TTL 15 минут)
3. Ввод номера телефона
4. Отправка SMS с кодом (4 цифры, TTL 10 минут)
5. Подтверждение обоих каналов
6. Активация аккаунта

Технические требования:
- Сервис отправки email: SendGrid/AWS SES
- Сервис SMS: Twilio/SMSC
- Rate limiting: 3 попытки в час на email/телефон
- Защита от ботов: reCAPTCHA v3

Шаг 2: Верификация личности (опционально, ₽2,000 для Free, включено в Premium)

Требуемые документы:
1. Паспорт РФ (разворот с фото) или ID другой страны
2. Селфи с паспортом и листком бумаги с текущей датой
3. ИНН (для РФ)

Процесс:
1. Пользователь загружает документы через защищенную форму
2. Автоматическая проверка качества фото (AI):
   - Читаемость текста
   - Отсутствие размытия
   - Соответствие формата
3. Автоматическое распознавание данных (OCR):
   - Извлечение ФИО, даты рождения, номера документа
   - Проверка контрольных сумм
4. Проверка в базах данных:
   - Черные списки
   - Санкционные списки (OFAC, EU, UN)
   - База недействительных паспортов МВД РФ (API)
5. Ручная модерация (если автопроверка не прошла):
   - Модератор проверяет документы (SLA: 24 часа)
   - Может запросить дополнительные документы
   - Принимает решение: одобрить/отклонить/запросить уточнения
6. Уведомление пользователя о результате
7. При одобрении: активация бейджа "Verified"

Критерии одобрения:
✓ Документ действителен
✓ Фото четкое и читаемое
✓ Селфи соответствует фото в документе (liveness detection)
✓ Данные не в черных списках
✓ Возраст 18+ (для инвесторов 21+)

Критерии отклонения:
✗ Поддельный документ
✗ Несоответствие селфи и фото в документе
✗ Документ в черном списке
✗ Недостаточное качество фото
✗ Возраст менее 18 лет

Шаг 3: Премиум-верификация (только для Enterprise-плана)

Дополнительные требования:
1. Справка о доходах (2-НДФЛ или выписка со счета)
2. Подтверждение источника средств для инвестиций
3. Видеозвонок с сотрудником платформы (15-20 минут)

Процесс видеозвонка:
1. Запись на удобное время через календарь
2. Проверка документов в реальном времени
3. Верификация liveness (пользователь выполняет действия: повернуть голову, моргнуть)
4. Краткое интервью:
   - Цели инвестирования
   - Опыт инвестиций
   - Источник средств
5. Запись звонка сохраняется (с согласия пользователя)
6. Решение принимается в течение 2 часов после звонка

Результат:
- Бейдж "Premium Verified" с галочкой и звездой
- Приоритет в откликах
- Доступ к закрытым сделкам (если предприниматель требует верификацию)

2.1.3. Процесс верификации для предпринимателей

Базовая верификация компании (обязательно для публикации объявлений)

Требуемые документы (для ООО/АО):
1. Выписка из ЕГРЮЛ (не старше 30 дней)
2. Устав компании
3. Свидетельство о регистрации (ОГРН)
4. ИНН компании
5. Паспорт директора/учредителя
6. Решение о назначении директора (если применимо)

Для ИП:
1. Выписка из ЕГРИП (не старше 30 дней)
2. Свидетельство о регистрации (ОГРНИП)
3. ИНН
4. Паспорт ИП

Процесс:
1. Загрузка документов через форму
2. Автоматическая проверка:
   - Валидация ИНН/ОГРН через API ФНС
   - Проверка статуса компании (действующая/ликвидирована)
   - Проверка в реестре дисквалифицированных лиц
   - Проверка задолженностей по налогам (API ФНС)
3. Проверка финансового состояния:
   - Выручка за последний год (из ЕГРЮЛ)
   - Наличие исполнительных производств
   - Кредитная история компании (опционально, через бюро)
4. Ручная модерация документов (SLA: 48 часов):
   - Проверка соответствия документов
   - Проверка полномочий загрузившего документы
   - Звонок на официальный телефон компании для подтверждения
5. Решение о верификации
6. Активация бейджа "Verified Business"

Критерии одобрения:
✓ Компания действующая
✓ Нет задолженностей по налогам >100,000 руб
✓ Директор не дисквалифицирован
✓ Документы актуальные и соответствуют друг другу
✓ Подтверждение по телефону пройдено

Критерии отклонения:
✗ Компания ликвидирована или в процессе ликвидации
✗ Массовый адрес регистрации (признак "однодневки")
✗ Директор-массовый руководитель (>10 компаний)
✗ Задолженность по налогам >500,000 руб
✗ Исполнительные производства на сумму >1 млн руб
✗ Компания моложе 3 месяцев (для Free-плана)

Расширенная верификация (для Premium)

Дополнительные документы:
1. Финансовая отчетность за последний год (баланс, ОПУ)
2. Бизнес-план (для стартапов)
3. Презентация проекта
4. Рекомендательные письма (опционально)

Дополнительные проверки:
1. Анализ финансовых показателей:
   - Рентабельность
   - Долговая нагрузка
   - Динамика выручки
2. Проверка репутации:
   - Отзывы в интернете
   - Судебные дела
   - Упоминания в СМИ
3. Видеозвонок с основателем/директором:
   - Презентация компании
   - Обсуждение планов развития
   - Проверка компетенций команды

Результат:
- Бейдж "Premium Verified Business" со звездой
- Приоритет в выдаче
- Увеличенное доверие инвесторов (+70% к откликам)
- Доступ к премиум-инвесторам

2.1.4. Техническая реализация

API для проверки документов:

// Интеграции
const verificationServices = {
  // OCR для распознавания документов
  ocr: {
    provider: "Google Cloud Vision API / ABBYY",
    confidence_threshold: 0.85,
    supported_documents: ["passport_rf", "inn", "snils", "driver_license"]
  },
  
  // Проверка в государственных реестрах
  government_apis: {
    fns: {
      endpoint: "https://api.nalog.ru/",
      methods: ["check_inn", "check_ogrn", "get_company_info"],
      rate_limit: "100 requests/hour"
    },
    egrul: {
      endpoint: "https://egrul.nalog.ru/",
      methods: ["get_extract", "check_status"],
      cache_ttl: "24 hours"
    },
    fssp: {
      endpoint: "https://api.fssp.gov.ru/",
      methods: ["check_enforcement_proceedings"],
      rate_limit: "50 requests/hour"
    }
  },
  
  // Проверка в санкционных списках
  sanctions: {
    providers: ["Dow Jones", "World-Check", "ComplyAdvantage"],
    update_frequency: "daily",
    lists: ["OFAC", "EU", "UN", "UK", "INTERPOL"]
  },
  
  // Liveness detection для селфи
  liveness: {
    provider: "Onfido / Sumsub",
    checks: ["face_match", "document_authenticity", "liveness"],
    confidence_threshold: 0.90
  },
  
  // Проверка телефона
  phone_verification: {
    provider: "Twilio Lookup",
    checks: ["validity", "carrier", "country", "line_type"],
    fraud_score_threshold: 0.7
  }
};

// Пример workflow верификации
async function verifyUser(userId, documents) {
  const results = {
    status: "pending",
    checks: {},
    score: 0,
    issues: []
  };
  
  // 1. OCR документов
  const ocrResults = await ocr.recognize(documents.passport);
  results.checks.ocr = {
    success: ocrResults.confidence > 0.85,
    data: ocrResults.extracted_data
  };
  
  // 2. Проверка в черных списках
  const sanctionsCheck = await sanctions.check(ocrResults.extracted_data.full_name);
  results.checks.sanctions = {
    success: !sanctionsCheck.found,
    lists_checked: sanctionsCheck.lists
  };
  
  // 3. Liveness detection
  const livenessCheck = await liveness.verify(documents.selfie, documents.passport);
  results.checks.liveness = {
    success: livenessCheck.match_score > 0.90,
    score: livenessCheck.match_score
  };
  
  // 4. Проверка документа в базе МВД
  const passportCheck = await government.checkPassport(ocrResults.extracted_data.passport_number);
  results.checks.passport_validity = {
    success: passportCheck.valid && !passportCheck.invalid,
    status: passportCheck.status
  };
  
  // 5. Расчет общего скора
  results.score = calculateVerificationScore(results.checks);
  
  // 6. Автоматическое решение
  if (results.score >= 0.95) {
    results.status = "approved";
    await approveVerification(userId);
  } else if (results.score < 0.70) {
    results.status = "rejected";
    results.issues = identifyIssues(results.checks);
    await rejectVerification(userId, results.issues);
  } else {
    results.status = "manual_review";
    await sendToModerator(userId, results);
  }
  
  return results;
}

// Расчет скора верификации
function calculateVerificationScore(checks) {
  const weights = {
    ocr: 0.15,
    sanctions: 0.30,
    liveness: 0.25,
    passport_validity: 0.30
  };
  
  let score = 0;
  for (const [check, weight] of Object.entries(weights)) {
    if (checks[check]?.success) {
      score += weight;
    }
  }
  
  return score;
}

База данных для хранения верификаций:

CREATE TABLE verifications (
    id UUID PRIMARY KEY,
    user_id UUID REFERENCES users(id),
    role_type VARCHAR(20), -- 'investor' или 'entrepreneur'
    verification_level VARCHAR(20), -- 'basic', 'verified', 'premium'
    status VARCHAR(20), -- 'pending', 'approved', 'rejected', 'expired'
    
    -- Документы
    documents JSONB, -- {passport: {url, type, uploaded_at}, selfie: {...}, ...}
    
    -- Результаты проверок
    checks_results JSONB, -- {ocr: {...}, sanctions: {...}, liveness: {...}}
    verification_score DECIMAL(3,2),
    
    -- Данные из документов
    extracted_data JSONB, -- {full_name, birth_date, passport_number, inn, ...}
    
    -- Модерация
    moderator_id UUID REFERENCES users(id),
    moderation_notes TEXT,
    moderation_completed_at TIMESTAMP,
    
    -- Временные метки
    submitted_at TIMESTAMP DEFAULT NOW(),
    approved_at TIMESTAMP,
    rejected_at TIMESTAMP,
    expires_at TIMESTAMP, -- верификация действительна 1 год
    
    -- Причина отклонения
    rejection_reason TEXT,
    rejection_code VARCHAR(50),
    
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- Индексы
CREATE INDEX idx_verifications_user_id ON verifications(user_id);
CREATE INDEX idx_verifications_status ON verifications(status);
CREATE INDEX idx_verifications_moderator ON verifications(moderator_id) WHERE status = 'manual_review';

-- Таблица для логов проверок
CREATE TABLE verification_checks_log (
    id UUID PRIMARY KEY,
    verification_id UUID REFERENCES verifications(id),
    check_type VARCHAR(50), -- 'ocr', 'sanctions', 'liveness', etc.
    provider VARCHAR(100),
    request_data JSONB,
    response_data JSONB,
    success BOOLEAN,
    error_message TEXT,
    duration_ms INTEGER,
    created_at TIMESTAMP DEFAULT NOW()
);

Примеры use-cases:

UC-2.1: Инвестор проходит базовую верификацию

1. Пользователь заходит в "Настройки" → "Верификация"
2. Видит преимущества верификации:
   - Увеличение доверия на 30%
   - Доступ к верифицированным предпринимателям
   - Приоритет в откликах
3. Нажимает "Начать верификацию"
4. Загружает паспорт (фото разворота с фото):
   - Система проверяет качество фото в реальном времени
   - Показывает подсказки: "Убедитесь, что текст читаемый"
   - Автоматически обрезает и выравнивает фото
5. Загружает селфи с паспортом:
   - Камера открывается с наложением контура лица
   - Система просит выполнить действие: "Поверните голову направо"
   - Делает фото автоматически при правильном положении
6. Система автоматически проверяет документы (30-60 секунд):
   - Показывается прогресс-бар с этапами проверки
   - "Распознаем данные..." → "Проверяем в базах..." → "Сравниваем фото..."
7. Результат:
   - Если автопроверка успешна (score > 0.95):
     * Мгновенное одобрение
     * Бейдж "Verified" активируется
     * Поздравительное сообщение
   - Если требуется ручная проверка (0.70 < score < 0.95):
     * "Ваши документы отправлены на проверку"
     * "Мы свяжемся с вами в течение 24 часов"
     * Email с подтверждением получения
   - Если отклонено (score < 0.70):
     * "К сожалению, верификация не пройдена"
     * Причина отклонения (например, "Фото паспорта нечеткое")
     * Возможность повторной попытки через 24 часа

UC-2.2: Предприниматель проходит верификацию компании

1. Пользователь создает профиль предпринимателя
2. Для публикации объявления требуется верификация компании
3. Заполняет форму с данными компании:
   - ИНН (автозаполнение остальных полей через API ФНС)
   - ОГРН
   - Юридический адрес
   - Фактический адрес
   - Телефон компании
   - Email компании
4. Загружает документы:
   - Выписка из ЕГРЮЛ (PDF, не старше 30 дней)
     * Система проверяет дату выписки автоматически
   - Устав компании (PDF)
   - Паспорт директора
5. Автоматическая проверка (2-3 минуты):
   - Валидация ИНН/ОГРН через API ФНС
   - Проверка статуса компании
   - Проверка директора в реестре дисквалифицированных лиц
   - Проверка задолженностей по налогам
6. Результаты автопроверки:
   - Показываются в виде чеклиста:
     ✓ Компания действующая
     ✓ Нет задолженностей по налогам
     ✓ Директор не дисквалифицирован
     ⏳ Документы отправлены на проверку модератору
7. Модератор проверяет документы (в течение 48 часов):
   - Звонит на официальный телефон компании
   - Проверяет соответствие документов
   - Принимает решение
8. Уведомление о результате:
   - Email + push-уведомление
   - Если одобрено:
     * Бейдж "Verified Business" активируется
     * Возможность публиковать объявления
   - Если отклонено:
     * Причина отклонения
     * Рекомендации по исправлению
     * Возможность повторной подачи

UC-2.3: Модератор проверяет документы

1. Модератор заходит в админ-панель → "Очередь верификаций"
2. Видит список заявок, отсортированных по приоритету:
   - Premium-пользователи (SLA 4 часа)
   - Обычные пользователи (SLA 24-48 часов)
3. Открывает заявку:
   - Слева: загруженные документы (с возможностью увеличения)
   - Справа: результаты автоматических проверок
   - Внизу: поле для заметок и кнопки действий
4. Проверяет документы:
   - Сравнивает фото в паспорте с селфи
   - Проверяет читаемость и подлинность документов
   - Проверяет соответствие данных
5. Если нужны уточнения:
   - Нажимает "Запросить дополнительные документы"
   - Выбирает из списка или пишет свободный текст
   - Пользователь получает уведомление с запросом
   - Заявка переходит в статус "Ожидание документов"
6. Принимает решение:
   - [Одобрить] - верификация активируется мгновенно
   - [Отклонить] - выбирает причину из списка + комментарий
   - [Заблокировать] - если обнаружено мошенничество
7. После решения:
   - Пользователь получает уведомление
   - Заявка архивируется
   - Статистика модератора обновляется

Приоритет: [CRITICAL] — MVP (базовая верификация), [HIGH] — v1.1 (премиум-верификация)

Обоснование решения:

  • Доверие: Верификация критична для B2B-платформы, где речь идет о больших суммах
  • Безопасность: Многоуровневая проверка минимизирует риск мошенничества
  • Автоматизация: 70-80% заявок проходят автоматически, снижая нагрузку на модераторов
  • Compliance: Соответствие требованиям 115-ФЗ (противодействие отмыванию денег)

Риски, которые закрывает:

  • Мошенничество и фейковые профили
  • Репутационные риски платформы
  • Юридические риски (платформа может быть привлечена к ответственности)
  • Недоверие между пользователями

Влияние на метрики:

  • Конверсия в отклики: +40-50% для верифицированных профилей
  • Успешность сделок: +60% при обоюдной верификации
  • Churn rate: -25% (верифицированные пользователи более лояльны)
  • Стоимость модерации: -70% за счет автоматизации

Связанные изменения:

  • Раздел «Профиль» — добавить отображение статуса верификации
  • Раздел «Поиск» — добавить фильтр по верифицированным пользователям
  • Раздел «Уведомления» — добавить алерты о статусе верификации
  • Раздел «Админ-панель» — добавить интерфейс модерации
  • Раздел «Безопасность» — добавить хранение документов с шифрованием

2.2. Система рейтингов и отзывов

Раздел ТЗ: Доверие и репутация

Суть проблемы:
Не описана механика оценок, не определено, кто может оставлять отзывы, отсутствует защита от накрутки рейтинга.

Предлагаемое решение:

2.2.1. Модель рейтинга

Многофакторный рейтинг:

Общий рейтинг пользователя = взвешенная сумма:

Для инвесторов:
- Отзывы от предпринимателей (40%)
- Активность на платформе (20%)
- Статус верификации (20%)
- Завершенные сделки (15%)
- Время отклика (5%)

Для предпринимателей:
- Отзывы от инвесторов (35%)
- Качество объявлений (25%)
- Статус верификации (20%)
- Прозрачность информации (10%)
- Время отклика (10%)

Шкала: от 1.0 до 5.0 (с точностью до 0.1)

Отображение рейтинга:

Визуализация:
★★★★☆ 4.7 (23 отзыва)

Детализация при наведении:
Коммуникация: ★★★★★ 4.9
Надежность: ★★★★☆ 4.6
Профессионализм: ★★★★☆ 4.5
Скорость ответа: ★★★★★ 5.0

2.2.2. Правила оставления отзывов

Кто может оставить отзыв:

Условия для возможности оставить отзыв:

1. Должно быть взаимодействие между пользователями:
   - Инвестор откликнулся на объявление предпринимателя
   - Предприниматель одобрил отклик
   - Состоялась переписка (минимум 5 сообщений от каждой стороны)
   - Прошло минимум 7 дней с момента первого контакта

2. Отзыв можно оставить только один раз на каждое взаимодействие

3. Отзыв можно оставить в течение 90 дней после взаимодействия

4. Нельзя оставить отзыв самому себе (очевидно)

5. Нельзя оставить отзыв, если пользователь заблокирован

6. Для Premium-пользователей: можно запросить отзыв через платформу
   (отправляется приглашение оставить отзыв)

Процесс оставления отзыва:

1. Система автоматически предлагает оставить отзыв:
   - Через 14 дней после начала взаимодействия
   - После завершения сделки (если пользователи отметили это)
   - Напоминание через 30 дней, если отзыв не оставлен

2. Форма отзыва:
   
   [Оцените взаимодействие с [Имя пользователя]]
   
   Общая оценка: ☆☆☆☆☆ (обязательно)
   
   Детальные оценки:
   - Коммуникация: ☆☆☆☆☆
   - Надежность: ☆☆☆☆☆
   - Профессионализм: ☆☆☆☆☆
   - Скорость ответа: ☆☆☆☆☆
   
   Текст отзыва: (необязательно, 50-1000 символов)
   [Расскажите о вашем опыте взаимодействия...]
   
   Рекомендации:
   ☐ Я рекомендую этого пользователя другим
   
   [Опубликовать отзыв] [Отменить]
   
   Примечание: Отзыв будет виден публично. Пожалуйста, будьте объективны
   и соблюдайте правила платформы.

3. Модерация отзыва:
   - Автоматическая проверка на запрещенные слова (мат, оскорбления)
   - Проверка на спам (одинаковые отзывы, подозрительные паттерны)
   - Если отзыв негативный (1-2 звезды), отправляется на ручную модерацию
   - SLA модерации: 24 часа

4. Публикация:
   - После модерации отзыв публикуется в профиле
   - Пользователь получает уведомление о новом отзыве
   - Рейтинг пересчитывается автоматически

2.2.3. Ответ на отзыв

Возможности:

1. Пользователь может ответить на отзыв один раз:
   - Ответ публикуется под отзывом
   - Максимум 500 символов
   - Модерируется так же, как отзыв

2. Пользователь может оспорить отзыв:
   - Если считает отзыв несправедливым или ложным
   - Заполняет форму с обоснованием
   - Модератор рассматривает спор (SLA: 48 часов)
   - Если отзыв признан нарушающим правила, он удаляется
   - Если отзыв справедлив, он остается, но добавляется пометка "Оспорен"

3. Пользователь может пожаловаться на отзыв:
   - Причины: оскорбления, спам, ложная информация, нарушение правил
   - Отзыв отправляется на проверку модератору
   - Решение принимается в течение 24 часов

2.2.4. Защита от накрутки рейтинга

Антифрод-механизмы:

# Псевдокод системы антифрода для отзывов

class ReviewAntiFraud:
    
    def check_review(review):
        fraud_score = 0
        flags = []
        
        # 1. Проверка на массовые отзывы
        recent_reviews = get_user_reviews(review.author_id, days=7)
        if len(recent_reviews) > 10:
            fraud_score += 30
            flags.append("mass_reviews")
        
        # 2. Проверка на отзывы без взаимодействия
        interaction = check_interaction(review.author_id, review.target_id)
        if not interaction or interaction.messages_count < 5:
            fraud_score += 50
            flags.append("no_interaction")
        
        # 3. Проверка на подозрительные паттерны текста
        similar_reviews = find_similar_reviews(review.text, threshold=0.8)
        if len(similar_reviews) > 3:
            fraud_score += 40
            flags.append("duplicate_text")
        
        # 4. Проверка на связь между аккаунтами
        if check_accounts_connection(review.author_id, review.target_id):
            fraud_score += 60
            flags.append("connected_accounts")
            # Связь определяется по: одинаковый IP, одинаковое устройство,
            # паттерны активности, платежные данные
        
        # 5. Проверка на новые аккаунты
        author_age = get_account_age(review.author_id)
        if author_age < 7:  # дней
            fraud_score += 20
            flags.append("new_account")
        
        # 6. Проверка на аномальную активность
        if check_abnormal_activity(review.author_id):
            fraud_score += 30
            flags.append("abnormal_activity")
            # Аномалии: только положительные/отрицательные отзывы,
            # отзывы только одному пользователю, и т.д.
        
        # 7. Проверка времени между взаимодействием и отзывом
        if interaction:
            time_diff = review.created_at - interaction.started_at
            if time_diff < timedelta(hours=1):
                fraud_score += 25
                flags.append("too_fast")
        
        # Решение
        if fraud_score >= 100:
            return {
                "action": "block",
                "reason": "High fraud probability",
                "flags": flags,
                "score": fraud_score
            }
        elif fraud_score >= 60:
            return {
                "action": "manual_review",
                "reason": "Suspicious activity",
                "flags": flags,
                "score": fraud_score
            }
        else:
            return {
                "action": "approve",
                "flags": flags,
                "score": fraud_score
            }
    
    def check_accounts_connection(user1_id, user2_id):
        # Проверка связи между аккаунтами
        
        # 1. Одинаковый IP-адрес
        user1_ips = get_user_ips(user1_id, days=30)
        user2_ips = get_user_ips(user2_id, days=30)
        if len(set(user1_ips) & set(user2_ips)) > 0:
            return True
        
        # 2. Одинаковое устройство (device fingerprint)
        user1_devices = get_user_devices(user1_id)
        user2_devices = get_user_devices(user2_id)
        if len(set(user1_devices) & set(user2_devices)) > 0:
            return True
        
        # 3. Одинаковые платежные данные
        user1_payments = get_payment_methods(user1_id)
        user2_payments = get_payment_methods(user2_id)
        if check_payment_similarity(user1_payments, user2_payments):
            return True
        
        # 4. Паттерны активности (одновременный онлайн)
        if check_activity_correlation(user1_id, user2_id) > 0.8:
            return True
        
        return False
    
    def check_abnormal_activity(user_id):
        reviews = get_all_user_reviews(user_id)
        
        # Только положительные отзывы (5 звезд)
        if all(r.rating == 5 for r in reviews) and len(reviews) > 5:
            return True
        
        # Только отрицательные отзывы (1-2 звезды)
        if all(r.rating <= 2 for r in reviews) and len(reviews) > 5:
            return True
        
        # Отзывы только одному пользователю или компании
        target_ids = [r.target_id for r in reviews]
        if len(set(target_ids)) == 1 and len(reviews) > 3:
            return True
        
        return False

Дополнительные меры защиты:

1. Ограничения на отзывы:
   - Максимум 5 отзывов в день от одного пользователя
   - Максимум 20 отзывов в месяц от одного пользователя
   - Нельзя оставить более 1 отзыва одному пользователю

2. Верификация для оставления отзывов:
   - Для Free-пользователей: только после верификации email и телефона
   - Для Premium: без ограничений

3. Вес отзывов:
   - Отзывы от верифицированных пользователей имеют вес 1.0
   - Отзывы от неверифицированных пользователей имеют вес 0.5
   - Отзывы от Premium-пользователей имеют вес 1.2
   - Отзывы от новых аккаунтов (<30 дней) имеют вес 0.7

4. Мониторинг и алерты:
   - Автоматические алерты модераторам при подозрительной активности
   - Еженедельные отчеты по фроду
   - Машинное обучение для улучшения детекции

5. Санкции за накрутку:
   - Первое нарушение: предупреждение + удаление фейковых отзывов
   - Второе нарушение: временная блокировка (30 дней)
   - Третье нарушение: перманентная блокировка аккаунта
   - Для организаторов накрутки: блокировка всех связанных аккаунтов

2.2.5. Техническая реализация

База данных:

CREATE TABLE reviews (
    id UUID PRIMARY KEY,
    author_id UUID REFERENCES users(id),
    target_id UUID REFERENCES users(id),
    interaction_id UUID REFERENCES interactions(id), -- связь с взаимодействием
    
    -- Оценки
    overall_rating DECIMAL(2,1) CHECK (overall_rating BETWEEN 1.0 AND 5.0),
    communication_rating DECIMAL(2,1),
    reliability_rating DECIMAL(2,1),
    professionalism_rating DECIMAL(2,1),
    response_time_rating DECIMAL(2,1),
    
    -- Текст
    review_text TEXT CHECK (char_length(review_text) BETWEEN 50 AND 1000),
    is_recommended BOOLEAN DEFAULT false,
    
    -- Статус
    status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'approved', 'rejected', 'disputed'
    moderation_notes TEXT,
    moderator_id UUID REFERENCES users(id),
    
    -- Антифрод
    fraud_score INTEGER DEFAULT 0,
    fraud_flags JSONB, -- массив флагов подозрительной активности
    
    -- Ответ на отзыв
    response_text TEXT,
    response_at TIMESTAMP,
    
    -- Споры
    is_disputed BOOLEAN DEFAULT false,
    dispute_reason TEXT,
    dispute_status VARCHAR(20), -- 'pending', 'resolved_kept', 'resolved_removed'
    
    -- Временные метки
    created_at TIMESTAMP DEFAULT NOW(),
    published_at TIMESTAMP,
    updated_at TIMESTAMP DEFAULT NOW(),
    
    -- Ограничения
    UNIQUE(author_id, target_id, interaction_id), -- один отзыв на взаимодействие
    CHECK(author_id != target_id) -- нельзя оставить отзыв самому себе
);

-- Индексы
CREATE INDEX idx_reviews_target ON reviews(target_id, status, published_at DESC);
CREATE INDEX idx_reviews_author ON reviews(author_id);
CREATE INDEX idx_reviews_moderation ON reviews(status) WHERE status = 'pending';
CREATE INDEX idx_reviews_fraud ON reviews(fraud_score) WHERE fraud_score > 60;

-- Таблица для хранения агрегированных рейтингов
CREATE TABLE user_ratings (
    user_id UUID PRIMARY KEY REFERENCES users(id),
    role_type VARCHAR(20), -- 'investor' или 'entrepreneur'
    
    -- Средние оценки
    overall_rating DECIMAL(3,2) DEFAULT 0,
    communication_rating DECIMAL(3,2) DEFAULT 0,
    reliability_rating DECIMAL(3,2) DEFAULT 0,
    professionalism_rating DECIMAL(3,2) DEFAULT 0,
    response_time_rating DECIMAL(3,2) DEFAULT 0,
    
    -- Статистика
    total_reviews INTEGER DEFAULT 0,
    positive_reviews INTEGER DEFAULT 0, -- 4-5 звезд
    neutral_reviews INTEGER DEFAULT 0, -- 3 звезды
    negative_reviews INTEGER DEFAULT 0, -- 1-2 звезды
    
    -- Распределение оценок
    rating_distribution JSONB, -- {1: 2, 2: 1, 3: 5, 4: 10, 5: 15}
    
    -- Дополнительные факторы
    verification_bonus DECIMAL(3,2) DEFAULT 0,
    activity_bonus DECIMAL(3,2) DEFAULT 0,
    
    updated_at TIMESTAMP DEFAULT NOW()
);

-- Триггер для автоматического пересчета рейтинга
CREATE OR REPLACE FUNCTION recalculate_rating()
RETURNS TRIGGER AS $$
BEGIN
    -- Пересчитываем рейтинг целевого пользователя
    UPDATE user_ratings
    SET 
        overall_rating = (
            SELECT AVG(overall_rating)
            FROM reviews
            WHERE target_id = NEW.target_id
              AND status = 'approved'
        ),
        total_reviews = (
            SELECT COUNT(*)
            FROM reviews
            WHERE target_id = NEW.target_id
              AND status = 'approved'
        ),
        -- ... остальные поля
        updated_at = NOW()
    WHERE user_id = NEW.target_id;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_recalculate_rating
AFTER INSERT OR UPDATE ON reviews
FOR EACH ROW
WHEN (NEW.status = 'approved')
EXECUTE FUNCTION recalculate_rating();

API endpoints:

// API для работы с отзывами

// Получить отзывы пользователя
GET /api/v1/users/:userId/reviews
Query params:
  - page: номер страницы (default: 1)
  - limit: количество на странице (default: 10, max: 50)
  - sort: сортировка (recent, rating_high, rating_low)
  - filter: фильтр по рейтингу (positive, neutral, negative, all)

Response:
{
  "data": [
    {
      "id": "uuid",
      "author": {
        "id": "uuid",
        "name": "Иван Иванов",
        "avatar": "url",
        "isVerified": true
      },
      "ratings": {
        "overall": 4.5,
        "communication": 5.0,
        "reliability": 4.0,
        "professionalism": 4.5,
        "responseTime": 5.0
      },
      "text": "Отличный инвестор, быстро отвечает...",
      "isRecommended": true,
      "createdAt": "2024-01-15T10:30:00Z",
      "response": {
        "text": "Спасибо за отзыв!",
        "createdAt": "2024-01-16T09:00:00Z"
      }
    }
  ],
  "meta": {
    "total": 23,
    "page": 1,
    "totalPages": 3
  },
  "summary": {
    "overallRating": 4.7,
    "totalReviews": 23,
    "ratingDistribution": {
      "5": 15,
      "4": 5,
      "3": 2,
      "2": 1,
      "1": 0
    }
  }
}

// Создать отзыв
POST /api/v1/reviews
Body:
{
  "targetUserId": "uuid",
  "interactionId": "uuid",
  "ratings": {
    "overall": 4.5,
    "communication": 5.0,
    "reliability": 4.0,
    "professionalism": 4.5,
    "responseTime": 5.0
  },
  "text": "Отличный инвестор...",
  "isRecommended": true
}

Response:
{
  "id": "uuid",
  "status": "pending", // или "approved" если прошел автомодерацию
  "message": "Отзыв отправлен на модерацию"
}

// Ответить на отзыв
POST /api/v1/reviews/:reviewId/response
Body:
{
  "text": "Спасибо за отзыв!"
}

// Оспорить отзыв
POST /api/v1/reviews/:reviewId/dispute
Body:
{
  "reason": "Отзыв содержит ложную информацию...",
  "evidence": ["url1", "url2"] // ссылки на доказательства
}

// Пожаловаться на отзыв
POST /api/v1/reviews/:reviewId/report
Body:
{
  "reason": "spam", // или "offensive", "false_info", "other"
  "details": "Подробности жалобы..."
}

Примеры use-cases:

UC-2.4: Инвестор оставляет отзыв предпринимателю

1. После 14 дней взаимодействия инвестор получает уведомление:
   "Вы общались с [Имя предпринимателя]. Оставьте отзыв о сотрудничестве"
   
2. Нажимает на уведомление, открывается форма отзыва:
   - Видит краткую информацию о взаимодействии (дата начала, количество сообщений)
   - Заполняет оценки по категориям (звездочки)
   - Пишет текстовый отзыв (опционально)
   - Ставит галочку "Рекомендую" (опционально)
   
3. Нажимает "Опубликовать отзыв"

4. Система проверяет отзыв:
   - Автоматическая проверка на спам и запрещенные слова
   - Проверка антифрод-системой (fraud_score = 15, низкий риск)
   - Отзыв одобряется автоматически
   
5. Отзыв публикуется мгновенно:
   - Появляется в профиле предпринимателя
   - Рейтинг пересчитывается автоматически
   - Предприниматель получает уведомление

6. Предприниматель видит новый отзыв:
   - Читает текст отзыва
   - Может ответить на отзыв
   - Может поблагодарить инвестора (кнопка "Спасибо")

UC-2.5: Предприниматель оспаривает негативный отзыв

1. Предприниматель получает уведомление о новом отзыве (2 звезды)

2. Читает отзыв и считает его несправедливым:
   "Не вышли на связь после первого сообщения"
   
3. Нажимает "Оспорить отзыв"

4. Заполняет форму оспаривания:
   "Инвестор написал одно сообщение и больше не отвечал на наши вопросы.
   Мы отправили 5 сообщений с уточнениями, но не получили ответа.
   Прикладываю скриншоты переписки."
   
5. Загружает доказательства (скриншоты переписки)

6. Отправляет на рассмотрение модератору

7. Модератор проверяет спор (в течение 48 часов):
   - Читает оба аргумента
   - Проверяет историю переписки в системе
   - Видит, что действительно предприниматель отправил 5 сообщений
   - Инвестор не отвечал после первого сообщения
   
8. Модератор принимает решение:
   - Отзыв признается необоснованным
   - Отзыв удаляется
   - Инвестор получает предупреждение
   - Рейтинг предпринимателя пересчитывается

9. Оба пользователя получают уведомления о решении

UC-2.6: Модератор проверяет подозрительные отзывы

1. Модератор заходит в админ-панель → "Отзывы на модерации"

2. Видит список отзывов, отсортированных по fraud_score:
   - Красные (score > 80): высокий риск
   - Желтые (score 60-80): средний риск
   - Зеленые (score < 60): низкий риск (автоодобренные)

3. Открывает отзыв с высоким fraud_score (85):
   Флаги:
   - connected_accounts (оба пользователя заходили с одного IP)
   - new_account (автор зарегистрирован 3 дня назад)
   - no_interaction (всего 2 сообщения в переписке)
   
4. Проверяет детали:
   - Смотрит историю IP-адресов обоих пользователей
   - Проверяет переписку (действительно, всего 2 коротких сообщения)
   - Смотрит другие отзывы автора (все 5 звезд, все одному пользователю)
   
5. Принимает решение: "Блокировать отзыв"
   - Отзыв удаляется
   - Автор получает предупреждение
   - Оба аккаунта помечаются как подозрительные
   - Если обнаружится еще одно нарушение, аккаунты будут заблокированы

6. Документирует решение в заметках модератора

7. Переходит к следующему отзыву в очереди

Приоритет: [HIGH] — v1.1

Обоснование решения:

  • Доверие: Система отзывов критична для B2B-платформы
  • Прозрачность: Многофакторный рейтинг дает полную картину
  • Защита: Антифрод-система минимизирует накрутку
  • Справедливость: Механизм оспаривания защищает от несправедливых отзывов

Риски, которые закрывает:

  • Накрутка рейтинга (фейковые отзывы)
  • Недоверие к платформе из-за необъективных оценок
  • Репутационные атаки (массовые негативные отзывы)
  • Злоупотребление системой отзывов

Влияние на метрики:

  • Конверсия в отклики: +35% для пользователей с рейтингом >4.5
  • Успешность сделок: +50% при высоких рейтингах обеих сторон
  • Доверие к платформе: +40% (по опросам пользователей)
  • Retention: +20% (пользователи с хорошим рейтингом более лояльны)

Связанные изменения:

  • Раздел «Профиль» — добавить отображение рейтинга и отзывов
  • Раздел «Поиск» — добавить фильтр и сортировку по рейтингу
  • Раздел «Уведомления» — добавить алерты о новых отзывах
  • Раздел «Админ-панель» — добавить интерфейс модерации отзывов
  • Раздел «Аналитика» — добавить метрики по отзывам и рейтингам

БЛОК 3: МАТЧМЕЙКИНГ И РЕКОМЕНДАЦИИ

3.1. Алгоритм подбора и рекомендаций

Раздел ТЗ: Поиск и фильтрация

Суть проблемы:
Не описан алгоритм подбора релевантных объявлений для инвесторов и инвесторов для предпринимателей. Отсутствует персонализация.

Предлагаемое решение:

3.1.1. Алгоритм ранжирования объявлений для инвесторов

Формула релевантности:

Relevance Score = 
  Profile Match (40%) +
  Engagement Signals (25%) +
  Freshness (15%) +
  Quality Score (10%) +
  Premium Boost (10%)

Где:

Profile Match = взвешенная сумма совпадений:
  - Отрасль (вес 30%)
  - Сумма инвестиций (вес 25%)
  - География (вес 20%)
  - Стадия бизнеса (вес 15%)
  - Форма участия (вес 10%)

Engagement Signals:
  - CTR объявления (вес 40%)
  - Количество откликов (вес 30%)
  - Время на странице (вес 20%)
  - Добавления в избранное (вес 10%)

Freshness:
  - Новые объявления (<24ч): коэффициент 1.5
  - Объявления 1-7 дней: коэффициент 1.2
  - Объявления 7-30 дней: коэффициент 1.0
  - Объявления >30 дней: коэффициент 0.8

Quality Score:
  - Полнота профиля предпринимателя (30%)
  - Верификация (30%)
  - Рейтинг (20%)
  - Качество объявления (20%)

Premium Boost:
  - Продвигаемые объявления: +50% к итоговому скору
  - Premium-предприниматели: +20% к итоговому скору

Псевдокод алгоритма:

def calculate_relevance_score(listing, investor_profile):
    # 1. Profile Match (40%)
    profile_match = 0
    
    # Отрасль
    if listing.industry in investor_profile.preferred_industries:
        profile_match += 0.30 * 0.40
    elif listing.industry in investor_profile.secondary_industries:
        profile_match += 0.15 * 0.40
    
    # Сумма инвестиций
    investment_match = calculate_investment_match(
        listing.investment_amount,
        investor_profile.min_investment,
        investor_profile.max_investment
    )
    profile_match += investment_match * 0.25 * 0.40
    
    # География
    if listing.location in investor_profile.preferred_locations:
        profile_match += 0.20 * 0.40
    elif listing.location.region in investor_profile.preferred_regions:
        profile_match += 0.10 * 0.40
    
    # Стадия бизнеса
    if listing.business_stage in investor_profile.preferred_stages:
        profile_match += 0.15 * 0.40
    
    # Форма участия
    if listing.participation_form in investor_profile.preferred_forms:
        profile_match += 0.10 * 0.40
    
    # 2. Engagement Signals (25%)
    engagement = 0
    
    # CTR (Click-Through Rate)
    listing_ctr = listing.clicks / listing.impressions if listing.impressions > 0 else 0
    avg_ctr = get_average_ctr()
    ctr_score = min(listing_ctr / avg_ctr, 2.0)  # cap at 2x average
    engagement += ctr_score * 0.40 * 0.25
    
    # Количество откликов
    response_rate = listing.responses / listing.views if listing.views > 0 else 0
    avg_response_rate = get_average_response_rate()
    response_score = min(response_rate / avg_response_rate, 2.0)
    engagement += response_score * 0.30 * 0.25
    
    # Время на странице
    avg_time = listing.total_time_spent / listing.views if listing.views > 0 else 0
    platform_avg_time = get_platform_average_time()
    time_score = min(avg_time / platform_avg_time, 2.0)
    engagement += time_score * 0.20 * 0.25
    
    # Добавления в избранное
    favorite_rate = listing.favorites / listing.views if listing.views > 0 else 0
    avg_favorite_rate = get_average_favorite_rate()
    favorite_score = min(favorite_rate / avg_favorite_rate, 2.0)
    engagement += favorite_score * 0.10 * 0.25
    
    # 3. Freshness (15%)
    age_hours = (now() - listing.created_at).total_seconds() / 3600
    if age_hours < 24:
        freshness = 1.5 * 0.15
    elif age_hours < 168:  # 7 days
        freshness = 1.2 * 0.15
    elif age_hours < 720:  # 30 days
        freshness = 1.0 * 0.15
    else:
        freshness = 0.8 * 0.15
    
    # 4. Quality Score (10%)
    quality = 0
    
    # Полнота профиля
    profile_completeness = calculate_profile_completeness(listing.entrepreneur)
    quality += profile_completeness * 0.30 * 0.10
    
    # Верификация
    if listing.entrepreneur.verification_level == "premium":
        quality += 1.0 * 0.30 * 0.10
    elif listing.entrepreneur.verification_level == "verified":
        quality += 0.7 * 0.30 * 0.10
    
    # Рейтинг
    rating_score = listing.entrepreneur.rating / 5.0
    quality += rating_score * 0.20 * 0.10
    
    # Качество объявления
    listing_quality = calculate_listing_quality(listing)
    quality += listing_quality * 0.20 * 0.10
    
    # 5. Premium Boost (10%)
    premium_boost = 0
    if listing.is_promoted:
        premium_boost += 0.50 * 0.10
    if listing.entrepreneur.tier == "premium":
        premium_boost += 0.20 * 0.10
    
    # Итоговый скор
    total_score = profile_match + engagement + freshness + quality + premium_boost
    
    # Персональные корректировки
    total_score = apply_personal_adjustments(total_score, listing, investor_profile)
    
    return total_score

def calculate_investment_match(amount, min_inv, max_inv):
    """Рассчитывает совпадение по сумме инвестиций"""
    if min_inv <= amount <= max_inv:
        return 1.0
    elif amount < min_inv:
        # Чем ближе к минимуму, тем выше скор
        ratio = amount / min_inv
        return max(0, ratio)
    else:  # amount > max_inv
        # Чем дальше от максимума, тем ниже скор
        ratio = max_inv / amount
        return max(0, ratio)

def apply_personal_adjustments(score, listing, investor_profile):
    """Применяет персональные корректировки на основе истории"""
    
    # Если инвестор уже взаимодействовал с похожими объявлениями
    similar_interactions = get_similar_interactions(investor_profile, listing)
    if similar_interactions:
        # Положительные взаимодействия (отклики, добавление в избранное)
        positive_count = sum(1 for i in similar_interactions if i.is_positive)
        # Отрицательные (скрытие, жалобы)
        negative_count = sum(1 for i in similar_interactions if i.is_negative)
        
        adjustment = (positive_count - negative_count) * 0.05
        score *= (1 + adjustment)
    
    # Если инвестор скрывал похожие объявления
    if check_hidden_similar(investor_profile, listing):
        score *= 0.5
    
    # Если инвестор добавлял в избранное похожие объявления
    if check_favorited_similar(investor_profile, listing):
        score *= 1.3
    
    return score

def calculate_listing_quality(listing):
    """Оценивает качество объявления"""
    quality = 0
    
    # Наличие фото/видео
    if listing.has_photos:
        quality += 0.2
    if listing.has_video:
        quality += 0.1
    
    # Длина описания
    description_length = len(listing.description)
    if description_length >= 500:
        quality += 0.2
    elif description_length >= 200:
        quality += 0.1
    
    # Наличие финансовых показателей
    if listing.has_financial_data:
        quality += 0.2
    
    # Наличие бизнес-плана
    if listing.has_business_plan:
        quality += 0.15
    
    # Наличие презентации
    if listing.has_presentation:
        quality += 0.15
    
    return min(quality, 1.0)

3.1.2. Персонализация рекомендаций

Машинное обучение для улучшения рекомендаций:

# Collaborative Filtering для рекомендаций

class RecommendationEngine:
    
    def __init__(self):
        self.user_item_matrix = None  # матрица взаимодействий пользователь-объявление
        self.model = None  # модель машинного обучения
    
    def train_model(self):
        """Обучает модель на исторических данных"""
        
        # 1. Собираем данные о взаимодействиях
        interactions = get_all_interactions()
        # Формат: user_id, listing_id, interaction_type, timestamp
        
        # 2. Создаем матрицу пользователь-объявление
        self.user_item_matrix = create_sparse_matrix(interactions)
        
        # 3. Обучаем модель (например, Matrix Factorization)
        self.model = AlternatingLeastSquares(
            factors=50,
            regularization=0.01,
            iterations=15
        )
        self.model.fit(self.user_item_matrix)
    
    def get_recommendations(self, user_id, n=20):
        """Получает персонализированные рекомендации для пользователя"""
        
        # 1. Получаем рекомендации от модели
        ml_recommendations = self.model.recommend(
            user_id,
            self.user_item_matrix[user_id],
            N=n*2  # берем в 2 раза больше для дальнейшей фильтрации
        )
        
        # 2. Получаем профиль пользователя
        user_profile = get_user_profile(user_id)
        
        # 3. Фильтруем по базовым критериям
        filtered = []
        for listing_id, score in ml_recommendations:
            listing = get_listing(listing_id)
            
            # Пропускаем, если не соответствует базовым критериям
            if not matches_basic_criteria(listing, user_profile):
                continue
            
            # Пропускаем, если пользователь уже взаимодействовал
            if has_interacted(user_id, listing_id):
                continue
            
            # Пропускаем скрытые объявления
            if is_hidden_by_user(user_id, listing_id):
                continue
            
            filtered.append((listing_id, score))
        
        # 4. Ре-ранжируем с учетом дополнительных факторов
        reranked = []
        for listing_id, ml_score in filtered[:n*1.5]:
            listing = get_listing(listing_id)
            
            # Рассчитываем итоговый скор
            relevance_score = calculate_relevance_score(listing, user_profile)
            
            # Комбинируем ML-скор и скор релевантности
            final_score = 0.6 * ml_score + 0.4 * relevance_score
            
            reranked.append((listing_id, final_score))
        
        # 5. Сортируем и возвращаем топ-N
        reranked.sort(key=lambda x: x[1], reverse=True)
        return reranked[:n]
    
    def get_similar_listings(self, listing_id, n=10):
        """Получает похожие объявления (для блока "Похожие предложения")"""
        
        # Используем item-to-item similarity
        similar = self.model.similar_items(listing_id, N=n+1)
        
        # Исключаем само объявление
        return [lid for lid, score in similar if lid != listing_id][:n]
    
    def update_online(self, user_id, listing_id, interaction_type):
        """Онлайн-обновление модели при новом взаимодействии"""
        
        # Добавляем взаимодействие в матрицу
        self.user_item_matrix[user_id, listing_id] = get_interaction_weight(interaction_type)
        
        # Частичное переобучение модели (incremental update)
        self.model.partial_fit(user_id, listing_id)

def get_interaction_weight(interaction_type):
    """Возвращает вес взаимодействия для модели"""
    weights = {
        "view": 1.0,
        "click": 2.0,
        "favorite": 5.0,
        "response": 10.0,
        "contact_request": 15.0,
        "hide": -5.0,
        "report": -10.0
    }
    return weights.get(interaction_type, 0)

Гибридный подход (Content-Based + Collaborative Filtering):

def get_hybrid_recommendations(user_id, n=20):
    """Гибридные рекомендации, комбинирующие разные подходы"""
    
    # 1. Collaborative Filtering (50%)
    cf_recommendations = recommendation_engine.get_recommendations(user_id, n=n)
    
    # 2. Content-Based (30%)
    user_profile = get_user_profile(user_id)
    cb_recommendations = get_content_based_recommendations(user_profile, n=n)
    
    # 3. Trending (10%)
    trending = get_trending_listings(n=n//2)
    
    # 4. Diversity (10%)
    diverse = get_diverse_recommendations(user_profile, n=n//2)
    
    # Комбинируем рекомендации
    combined = {}
    
    for listing_id, score in cf_recommendations:
        combined[listing_id] = combined.get(listing_id, 0) + score * 0.50
    
    for listing_id, score in cb_recommendations:
        combined[listing_id] = combined.get(listing_id, 0) + score * 0.30
    
    for listing_id, score in trending:
        combined[listing_id] = combined.get(listing_id, 0) + score * 0.10
    
    for listing_id, score in diverse:
        combined[listing_id] = combined.get(listing_id, 0) + score * 0.10
    
    # Сортируем и возвращаем топ-N
    sorted_recommendations = sorted(
        combined.items(),
        key=lambda x: x[1],
        reverse=True
    )
    
    return sorted_recommendations[:n]

def get_content_based_recommendations(user_profile, n=20):
    """Рекомендации на основе профиля пользователя"""
    
    # Получаем все активные объявления
    all_listings = get_active_listings()
    
    # Рассчитываем релевантность для каждого
    scored_listings = []
    for listing in all_listings:
        score = calculate_relevance_score(listing, user_profile)
        scored_listings.append((listing.id, score))
    
    # Сортируем и возвращаем топ-N
    scored_listings.sort(key=lambda x: x[1], reverse=True)
    return scored_listings[:n]

def get_trending_listings(n=10):
    """Получает трендовые объявления (популярные за последние 24 часа)"""
    
    # Получаем объявления с высоким engagement за последние 24 часа
    trending = db.query("""
        SELECT 
            listing_id,
            COUNT(DISTINCT user_id) as unique_views,
            SUM(CASE WHEN interaction_type = 'favorite' THEN 5
                     WHEN interaction_type = 'response' THEN 10
                     WHEN interaction_type = 'click' THEN 2
                     ELSE 1 END) as engagement_score
        FROM interactions
        WHERE created_at >= NOW() - INTERVAL '24 hours'
        GROUP BY listing_id
        ORDER BY engagement_score DESC
        LIMIT :n
    """, n=n)
    
    return [(row['listing_id'], row['engagement_score']) for row in trending]

def get_diverse_recommendations(user_profile, n=10):
    """Получает разнообразные рекомендации (из разных отраслей/регионов)"""
    
    # Получаем отрасли, с которыми пользователь еще не взаимодействовал
    unexplored_industries = get_unexplored_industries(user_profile)
    
    recommendations = []
    per_industry = max(1, n // len(unexplored_industries))
    
    for industry in unexplored_industries:
        # Получаем лучшие объявления из этой отрасли
        top_in_industry = get_top_listings_in_industry(industry, per_industry)
        recommendations.extend(top_in_industry)
    
    return recommendations[:n]

3.1.3. Реализация на фронтенде

Блоки рекомендаций:

// Компонент главной страницы для инвестора

function InvestorDashboard() {
  return (
    <div className="dashboard">
      {/* 1. Персонализированные рекомендации */}
      <RecommendationsBlock
        title="Рекомендуем для вас"
        subtitle="На основе ваших предпочтений и активности"
        endpoint="/api/v1/recommendations/personalized"
        icon="✨"
      />
      
      {/* 2. Новые объявления */}
      <RecommendationsBlock
        title="Новые предложения"
        subtitle="Опубликованы за последние 24 часа"
        endpoint="/api/v1/listings/recent"
        icon="🆕"
      />
      
      {/* 3. Трендовые */}
      <RecommendationsBlock
        title="Популярные сейчас"
        subtitle="Объявления с высоким интересом"
        endpoint="/api/v1/listings/trending"
        icon="🔥"
      />
      
      {/* 4. Из избранных отраслей */}
      <RecommendationsBlock
        title="IT и технологии"
        subtitle="Ваша любимая отрасль"
        endpoint="/api/v1/listings/by-industry/it"
        icon="💻"
      />
      
      {/* 5. Похожие на избранные */}
      <RecommendationsBlock
        title="Похожие на избранные"
        subtitle="На основе объявлений в вашем избранном"
        endpoint="/api/v1/recommendations/similar-to-favorites"
        icon="⭐"
      />
      
      {/* 6. Разнообразие */}
      <RecommendationsBlock
        title="Откройте новое"
        subtitle="Объявления из других отраслей"
        endpoint="/api/v1/recommendations/diverse"
        icon="🌍"
      />
    </div>
  );
}

// Компонент блока рекомендаций
function RecommendationsBlock({ title, subtitle, endpoint, icon }) {
  const [listings, setListings] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchRecommendations();
  }, [endpoint]);
  
  const fetchRecommendations = async () => {
    try {
      const response = await api.get(endpoint);
      setListings(response.data);
    } catch (error) {
      console.error('Error fetching recommendations:', error);
    } finally {
      setLoading(false);
    }
  };
  
  if (loading) {
    return <SkeletonLoader />;
  }
  
  return (
    <div className="recommendations-block">
      <div className="block-header">
        <span className="icon">{icon}</span>
        <div>
          <h2>{title}</h2>
          <p className="subtitle">{subtitle}</p>
        </div>
        <a href={`/listings?source=${endpoint}`} className="view-all">
          Смотреть все →
        </a>
      </div>
      
      <div className="listings-carousel">
        {listings.map(listing => (
          <ListingCard
            key={listing.id}
            listing={listing}
            onView={() => trackView(listing.id)}
            onFavorite={() => trackFavorite(listing.id)}
          />
        ))}
      </div>
    </div>
  );
}

// Отслеживание взаимодействий для улучшения рекомендаций
function trackView(listingId) {
  api.post('/api/v1/interactions', {
    listingId,
    type: 'view',
    timestamp: new Date().toISOString()
  });
  
  // Онлайн-обновление модели рекомендаций
  recommendation_engine.update_online(currentUserId, listingId, 'view');
}

function trackFavorite(listingId) {
  api.post('/api/v1/interactions', {
    listingId,
    type: 'favorite',
    timestamp: new Date().toISOString()
  });
  
  recommendation_engine.update_online(currentUserId, listingId, 'favorite');
}

Примеры use-cases:

UC-3.1: Инвестор заходит на главную страницу

1. Пользователь авторизуется и попадает на дашборд

2. Система загружает персонализированные рекомендации:
   - Запрос к recommendation engine
   - Получение топ-20 релевантных объявлений
   - Группировка по блокам (персональные, новые, трендовые, etc.)

3. Отображаются блоки рекомендаций:
   
   ✨ Рекомендуем для вас
   [Карточка 1] [Карточка 2] [Карточка 3] [Карточка 4] [Смотреть все →]
   
   🆕 Новые предложения
   [Карточка 5] [Карточка 6] [Карточка 7] [Карточка 8] [Смотреть все →]
   
   🔥 Популярные сейчас
   [Карточка 9] [Карточка 10] [Карточка 11] [Карточка 12] [Смотреть все →]

4. Пользователь взаимодействует с объявлениями:
   - Кликает на карточку → открывается детальная страница
   - Добавляет в избранное → система учитывает для будущих рекомендаций
   - Скрывает объявление → система исключает похожие из рекомендаций

5. Система отслеживает взаимодействия:
   - Логирует просмотры, клики, избранное
   - Обновляет модель рекомендаций в реальном времени
   - Корректирует будущие рекомендации на основе поведения

UC-3.2: Инвестор просматривает объявление

1. Пользователь открывает детальную страницу объявления

2. Внизу страницы отображается блок "Похожие предложения":
   
   📊 Похожие предложения
   [Карточка 1] [Карточка 2] [Карточка 3] [Карточка 4]
   
   Критерии подбора:
   - Та же отрасль (IT)
   - Похожая сумма инвестиций (±30%)
   - Та же стадия бизнеса (рост)
   - Похожая форма участия (доля в бизнесе)

3. Пользователь кликает на одно из похожих объявлений

4. Система логирует:
   - "Пользователь перешел с объявления A на объявление B"
   - Увеличивает связь между этими объявлениями в модели
   - Учитывает для будущих рекомендаций

5. Если пользователь откликается на похожее объявление:
   - Система понимает, что рекомендация была успешной
   - Увеличивает вес этого типа рекомендаций для пользователя

UC-3.3: Предприниматель получает рекомендации инвесторов

1. Предприниматель публикует объявление

2. Система анализирует объявление:
   - Извлекает ключевые параметры (отрасль, сумма, стадия, etc.)
   - Ищет инвесторов с подходящими предпочтениями
   - Ранжирует инвесторов по релевантности

3. Предприниматель видит блок "Подходящие инвесторы":
   
   💼 Инвесторы, которым может быть интересно ваше предложение
   
   [Карточка инвестора 1]
   Иван Иванов
   ★★★★★ 4.9 (15 отзывов)
   ✓ Verified
   Интересы: IT, E-commerce
   Сумма инвестиций: от 5 до 50 млн ₽
   [Пригласить к диалогу]
   
   [Карточка инвестора 2]
   ...

4. Предприниматель может:
   - Просмотреть профиль инвестора
   - Отправить приглашение к диалогу (если Premium)
   - Добавить в избранное для отслеживания

5. Система отправляет уведомления инвесторам:
   "Новое объявление, которое может вас заинтересовать"
   - Персонализированное сообщение
   - Ссылка на объявление
   - Краткое описание, почему это релевантно

Приоритет: [HIGH] — v1.1 (базовый алгоритм), [MEDIUM] — v1.2 (ML-рекомендации)

Обоснование решения:

  • Релевантность: Персонализированные рекомендации увеличивают вероятность успешного матчинга
  • Engagement: Пользователи проводят больше времени на платформе, просматривая релевантный контент
  • Конверсия: Правильные рекомендации увеличивают количество откликов и сделок
  • Retention: Пользователи возвращаются, зная, что найдут подходящие предложения

Риски, которые закрывает:

  • Низкая конверсия из-за нерелевантных объявлений
  • Информационная перегрузка (слишком много объявлений)
  • Холодный старт для новых пользователей
  • Эхо-камера (пользователь видит только одно и то же)

Влияние на метрики:

  • CTR (Click-Through Rate): +40-60% для персонализированных рекомендаций
  • Конверсия в отклики: +30-50%
  • Время на платформе: +35%
  • Retention (D7): +25%
  • Успешность матчинга: +45%

Связанные изменения:

  • Раздел «Главная страница» — добавить блоки рекомендаций
  • Раздел «Детальная страница объявления» — добавить «Похожие предложения»
  • Раздел «Поиск» — добавить персонализированную сортировку
  • Раздел «Аналитика» — добавить метрики по рекомендациям
  • Раздел «Инфраструктура» — добавить ML-сервис для рекомендаций