# Дизайн интеграции Лидерра ↔ Поставщик crm.bp-gr.ru

**Дата:** 2026-05-10
**Статус:** черновик дизайна (brainstorming output)
**Заказчик:** Дмитрий (владелец Лидерры)

## 1. Контекст

Лидерра — реселлер лидов. Покупает поток номеров у поставщика [crm.bp-gr.ru](https://crm.bp-gr.ru) (бренд «BG») и перепродаёт своим тенантам через собственный портал.

У поставщика нет публичного REST API. Доступны:

- **PUSH webhook** для приёма лидов: настройка на [/admin/user/api](https://crm.bp-gr.ru/admin/user/api), payload `{vid, project, tag, phone, phones, time}`.
- **Внутренние AJAX endpoints** для управления проектами: `rt-project-save`, `rt-project-update`, `rt-project-delete`, `rt-projects-load` и т.д. Требуют cookie-сессии и CSRF-токена.

## 2. Модель данных

### 2.1. Проект Лидерры

Один проект Лидерры = один сигнал-источник лидов:

| Тип сигнала | Идентификатор | Пример |
|---|---|---|
| **Сайт** | домен конкурента | `vashinvestor.ru` |
| **Звонок** | номер конкурента (11 цифр, начинается с 7) | `7XXXXXXXXXX` |
| **СМС** | пара (имя отправителя, ключевое слово) | sender=`TINKOFF`, keyword=`ипотека` |

Поля проекта:

- `id`, `tenant_id` (клиент Лидерры), `name`, `signal_type`, `signal_identifier`
- `daily_limit` (число лидов в день, заказ от клиента)
- `workdays` (массив 1–7 = Пн–Вс)
- `regions` (массив кодов регионов РФ; пустой = все регионы)
- `status` (`active` / `paused` / `archived`)
- `created_at`, `updated_at`

Для СМС вместо одного `signal_identifier` хранится:

- `sms_senders[]` — массив имён отправителей (буквы / цифры до 6, не 11-значные)
- `sms_keyword` — фраза/слово в SMS (опциональное)

### 2.2. Поставщик-проект

Уникальность у поставщика зависит от типа сигнала и платформы:

| Тип | Платформы | Ключ уникальности supplier-проекта |
|---|---|---|
| Сайт | B1, B2, B3 | `(signal_identifier)` = домен |
| Звонок | B1, B2, B3 | `(signal_identifier)` = номер |
| СМС | **B2** (B1 недоступен) | `(sms_senders, sms_keyword)` |
| СМС | **B3** (B1 недоступен) | `(sms_senders)` — keyword игнорируется |

**Правило объединения для СМС:**

- На B3 все Лидерра-проекты с одним и тем же отправителем (любым keyword'ом) шарят один supplier-проект.
- На B2 Лидерра-проекты с одним отправителем, но разными keyword'ами — отдельные supplier-проекты.
- Если у Лидерра-клиента keyword пуст — он подключается только к B3, на B2 не идёт.

В Лидерре отдельная сущность `supplier_projects` (агрегатная):

- `id`, `platform` (`B1`/`B2`/`B3`), `signal_type`, `unique_key` (домен / номер / `sender+keyword` / `sender`)
- `supplier_id` (внутренний id у поставщика)
- `current_limit`, `current_workdays` (синхронизировано с поставщиком)
- `last_synced_at`, `sync_status` (`ok` / `pending` / `failed`)

Маппинг N:M — один Лидерра-проект СМС может ссылаться на 1 проект B2 + 1 проект B3 (или только B3, если keyword пуст). Проекты Сайт/Звонок ссылаются на 3 supplier-проекта (B1+B2+B3).

### 2.3. Sharing-модель лидов

Один входящий лид от поставщика → потенциально создаёт несколько `deal`-записей (по одной на каждого клиента Лидерры, активного на этом источнике сегодня и не исчерпавшего квоту).

Каждая deal-копия:

- независимый статус, комментарии, история
- ссылается на исходный `supplier_lead` (для трассировки)

## 3. Жизненный цикл проекта Лидерры

### 3.1. Создание

1. Клиент Лидерры в UI указывает `signal_type`, `signal_identifier`, `daily_limit`, `workdays`.
2. Лидерра валидирует формат идентификатора:
   - **Сайт:** валидный домен (regex `^[a-z0-9-]+(\.[a-z0-9-]+)+$` нижний регистр)
   - **Звонок:** 11 цифр, начинается с `7`
   - **СМС sender:** 1–30 символов, буквы латиницы/цифры (до 6 цифр — короткий номер); 11-значные номера отвергаем (ограничение поставщика)
   - **СМС keyword:** 1–60 символов, любые символы; опционально (если пусто — используется только B3)
3. Лидерра ищет существующий `supplier_projects` по `(signal_type, signal_identifier)`:
   - **Если есть:** просто привязывает новый Лидерра-проект к нему. Push настроек у поставщика — следующим cron'ом.
   - **Если нет:** ставит задачу `create_supplier_project` в очередь синхронизации (см. §4).

### 3.2. Изменение

Клиент может править:

- `daily_limit` (количество)
- `workdays` (дни сбора)
- `status` (active ↔ paused — пауза без удаления)

Все правки немедленно сохраняются в Лидерре. На поставщика — следующим cron'ом.

### 3.3. Удаление (архивирование)

Soft delete: `status = 'archived'`.

**Lifecycle на стороне supplier_project:**

- Как только у supplier_project не остаётся активных Лидерра-проектов (все паузнули/удалили) → следующим cron'ом выключаем у поставщика (`status=false` через `rt-project-update`). Запоминаем `inactive_since`.
- Если активный клиент возвращается до истечения TTL — supplier_project переиспользуется (просто `status=true` + актуальные limit/workdays).
- Через **180 дней** без активных клиентов supplier_project удаляется у поставщика (`rt-project-delete`) и в нашей БД.

### 3.4. UI-подсказка

В форме создания/редактирования отображается:

> Лиды равномерно распределяются по 3 платформам сбора. Чтобы увеличить приток — увеличьте общее количество. Изменения применятся со следующего дня.

## 4. Синхронизация Лидерра → Поставщик

### 4.1. Cron-задача в 20:30 МСК

Каждый день в 20:30 МСК Лидерра запускает `SyncSupplierProjects`:

1. Для каждого `supplier_projects` — собирает все активные Лидерра-проекты, привязанные к нему.
2. Считает на день D+1:
   - `total_quota[day]` = сумма `daily_limit` клиентов, у которых день `D+1` в `workdays`
   - `target_workdays` = объединение всех `workdays` активных клиентов
3. Распределяет `total_quota` по платформам:
   - **Сайт / Звонок** (3 платформы): `B1 = ⌈total/3⌉`, `B2 = ⌈(total-B1)/2⌉`, `B3 = total-B1-B2`
   - **СМС с keyword** (B2+B3): `B2 = ⌈total/2⌉`, `B3 = ⌊total/2⌋`
   - **СМС без keyword** (только B3): `B3 = total`
   - Остаток после деления отдаётся приоритетом старшим платформам (B1 → B2)
4. Объединяет геофильтр: `regions` у поставщика = объединение `regions` всех активных Лидерра-проектов на этом источнике; `regions_reverse` всегда `false` (включающий список).
5. Сравнивает с текущими настройками поставщика:
   - Если изменилось — POST `rt-project-update` для каждой из 3 платформ.
   - Если новый источник — POST `rt-project-save`.
   - Если активных клиентов 0 — выключение/удаление.
6. Запас 30 минут до 21:00 МСК (дедлайн поставщика).

### 4.2. Cutoff для клиента

- До 20:30 МСК — изменения попадают в текущий cron, применятся завтра.
- После 20:30 МСК — изменения сохраняются, в UI баннер «Применится послезавтра», следующий cron подхватит.
- 22:00–00:00 МСК — у поставщика API заблокирован (для контекста; cron в 20:30 не попадает).

### 4.3. Резервирование push'а настроек

**Основной канал:** AJAX-автоматизация через cookie-сессию + CSRF.

**Поддержание сессии:** отдельный cron-job `RefreshSupplierSession` (запускается каждый час и за 15 мин до основного 20:30 cron'а):

1. Headless Playwright заходит на crm.bp-gr.ru.
2. Если уже залогинен (по предыдущей cookie из Redis) — извлекает свежий CSRF-токен со страницы.
3. Если нет — логинится с credentials из `.env` (`SUPPLIER_LOGIN`, `SUPPLIER_PASSWORD`, защищены через Laravel encrypter).
4. Сохраняет `cookie` (PHPSESSID) и `csrf_token` в Redis с TTL 6 часов.
5. Основной sync-cron читает их оттуда и подмешивает в HTTP-запросы к `rt-*` endpoints.
6. При 401/403 от поставщика — Лидерра запускает `RefreshSupplierSession` внепланово.

**Резерв (при сбоях):**

1. **Очередь pending-changes**: каждое неуспешное обновление помещается в очередь, повтор каждые 10 минут до 21:00.
2. **Алерт менеджеру**: если очередь не пуста к 21:00 — уведомление в Telegram/email с инструкцией ручной правки через UI поставщика.
3. **Откат к предыдущему успешному состоянию**: если новые настройки не дошли — клиенты Лидерры работают на вчерашних квотах (известное состояние, никакой потери).
4. **Health-check сессии**: перед cron'ом проверяем `rt-projects-load` — если 401/403, заранее обновляем cookie/CSRF.

## 5. Приём лидов Поставщик → Лидерра

### 5.1. Основной канал — webhook

Поставщик POST'ит на `https://liderra.ru/webhook/{tenant_token}` (общий URL, не per-client).

Payload:

```json
{
  "vid": 432176649,
  "project": "B1_vashinvestor.ru",
  "tag": "Ваш инвестор",
  "phone": "7XXXXXXXXXX",
  "phones": ["7XXXXXXXXXX"],
  "time": 1703781939
}
```

Обработчик (`HandleSupplierWebhook`):

1. Проверяет защиту в два слоя:
   - **IP allowlist** — запрос отвергается, если IP отправителя не из списка известных IP поставщика (хранится в `settings.supplier_ip_allowlist`, обновляется админом).
   - **Секретный токен в URL** — `tenant_token` это длинный случайный секрет (≥32 символа), генерируется при регистрации интеграции; известен только Лидерре и поставщику. Неверный токен → 404 (не 403, чтобы не палить существование endpoint'а).
2. Парсит `project` → определяет `(signal_type, signal_identifier)`:
   - префикс `B1_` / `B2_` / `B3_` → B-платформа
   - остальная часть → identifier
3. Сохраняет `supplier_lead` (raw payload + метаданные).
4. Запускает `RouteLead` (см. §6).
5. Возвращает 200 OK.

### 5.2. Резерв — CSV reconciliation

**Раз в час** (или 4 раза в день) Лидерра запрашивает CSV-экспорт `/admin/report/index?type=49` (или archive endpoint) за последние сутки и сверяет:

1. Список `vid` из CSV vs `supplier_leads` в БД.
2. Если в CSV есть `vid`, которого нет у нас — pending: восстанавливаем с тем же `RouteLead` flow (но помечаем как `recovered_from_csv`).
3. Если расхождение > 5% за день — алерт менеджеру.

Это даёт стабильность даже при кратковременных сбоях нашего webhook'а.

## 6. Routing к клиентам Лидерры

При поступлении лида (`RouteLead`):

1. Находим `supplier_projects` по `(signal_type, signal_identifier)`.
2. Определяем регион номера по коду телефона (справочник DEF-кодов).
3. Берём список Лидерра-проектов на этом источнике с условиями:
   - `status = 'active'`
   - сегодняшний день в `workdays`
   - не превышен дневной счётчик: `delivered_today < daily_limit`
   - регион номера попадает в `regions` клиента (или `regions` пуст = все)
4. Сортируем по `created_at ASC` (старые проекты первыми — детерминированный порядок).
5. Для каждого подходящего проекта:
   - Создаём `deal` с независимой копией данных лида.
   - Инкрементируем `delivered_today` и `delivered_in_month`.
   - Списываем с баланса по текущей ступени `pricing_tiers`.
   - Шлём уведомление клиенту по его настройкам.
6. Один и тот же лид → может попасть N клиентам (sharing).
7. Если клиент уже исчерпал квоту — пропускаем.

### 6.1. Reset счётчиков

Cron в 00:00 МСК сбрасывает `delivered_today = 0` для всех Лидерра-проектов.

## 7. Биллинг и тарифы клиентам Лидерры

### 7.1. Закупка у поставщика

Поставщик берёт **фиксированную цену** за лид (значение хранится в `settings`, поле редактируемое админом Лидерры).

### 7.2. Тариф для клиентов Лидерры — ступенчатый (volume tiers)

Клиент Лидерры платит по ступенчатой схеме: чем больше лидов получено в текущем месяце — тем дешевле каждый следующий.

**Структура** (таблица `pricing_tiers`, конфигурируется админом Лидерры):

- `tier_no` — номер ступени (1..7)
- `leads_in_tier` — сколько лидов покрывает эта ступень (для последней — может быть `NULL` = «всё свыше»)
- `price_per_lead` — цена за один лид в рублях

И объём ступени, и цена редактируются в админке Лидерры. Изменения тарифной сетки применяются с 1-го числа следующего месяца (кроме ситуации когда изменение увеличивает цену — тут TBD по бизнесу).

### 7.3. Логика начисления

- Счётчик `delivered_in_month` для каждого `(tenant_id)` обнуляется автоматически 1-го числа каждого месяца в 00:00 МСК.
- При доставке очередного лида: `delivered_in_month++`, цена этого лида = `price_per_lead` той ступени, в которую попадает текущий `delivered_in_month`.
- Списание с баланса клиента происходит немедленно по доставке (атомарно с созданием `deal`).
- Если баланс недостаточен — лид не доставляется, проект автоматически переходит в `paused` до пополнения; уведомление клиенту.

### 7.4. История

Хранится в таблице `lead_charges`: `tenant_id, deal_id, tier_no, price_per_lead, charged_at`. Используется для отчётов клиенту и аудита.

## 8. Бизнес-правила и инварианты

- **Источников у поставщика на одного `(signal_type, signal_identifier)` всегда ровно 3** (B1/B2/B3).
- **Лимит у поставщика равен `Total/3` с приоритетом остатка B1→B2.**
- **Workdays у поставщика = объединение workdays активных Лидерра-клиентов.**
- **Если у `supplier_project` 0 активных Лидерра-проектов — выключаем у поставщика, не удаляем сразу** (теплый кэш на случай быстрого возврата).

## 9. Схема БД (предварительно)

Новые таблицы:

- `signal_sources` (источники: домены/номера/SMS-тексты)
- `liderra_projects` (проекты клиентов Лидерры)
- `supplier_projects` (агрегатное состояние у поставщика)
- `supplier_leads` (raw payload от поставщика)
- `liderra_project_deliveries` (счётчики delivered_today, history)
- `supplier_sync_jobs` (очередь и история синхронизаций)

Существующая таблица `deals` дополняется FK на `liderra_project` и `supplier_lead`.

Точная DDL — отдельным шагом после утверждения дизайна.

## 10. Открытые вопросы

| # | Вопрос | Кто решает |
|---|---|---|
| 1 | ~~Правила валидации SMS-содержания~~ | ✅ закрыт 10.05.2026 (см. §2.1, §3.1) |
| 2 | ~~Цена за лид от поставщика — фиксирована или по типу B1/B2/B3?~~ | ✅ закрыт 10.05.2026: фикс. (см. §7.1) |
| 3 | ~~Как Лидерра билит клиентов: за каждый delivered лид или подписочно?~~ | ✅ закрыт 10.05.2026: 7-ступенчатый volume tier, месячный сброс (см. §7.2–7.4) |
| 4 | ~~Геофильтр (регионы) — поддерживаем уже сейчас или v2?~~ | ✅ закрыт 10.05.2026: MVP, объединение на supplier + фильтр на нашей стороне (см. §2.1, §4.1, §6) |
| 5 | ~~HMAC-подпись поставщика — есть или только IP allowlist?~~ | ✅ закрыт 10.05.2026: defense-in-depth (IP allowlist + секретный токен в URL, см. §5.1) |
| 6 | ~~Точный механизм cookie/CSRF refresh для AJAX-автоматизации~~ | ✅ закрыт 10.05.2026: headless Playwright cron каждый час + по 401/403 (см. §4.3) |
| 7 | ~~TTL для «теплого кэша» выключенных supplier_projects~~ | ✅ закрыт 10.05.2026: сразу выключение, удаление через 180 дней (см. §3.3) |

## 11. План реализации

После подтверждения дизайна — следующий шаг через skill `writing-plans`: декомпозиция на спринты, schema-миграции, контроллеры, jobs, тесты.
