Q1 SMS validation: B2 = (sender, keyword), B3 = sender only; B1 not available Q2 Supplier pricing: фиксированная цена за лид Q3 Liderra billing: 7-tier volume pricing, monthly reset (config in admin) Q4 Geo filter: MVP, union at supplier + filter on our side Q5 Webhook auth: defense-in-depth (IP allowlist + secret token in URL) Q6 Cookie/CSRF refresh: hourly headless Playwright cron + reactive on 401/403 Q7 supplier_project TTL: immediate disable, delete after 180 days no activity cspell-words.txt: +геофильтр, логинится, encrypter, PHPSESSID Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
21 KiB
Дизайн интеграции Лидерра ↔ Поставщик crm.bp-gr.ru
Дата: 2026-05-10 Статус: черновик дизайна (brainstorming output) Заказчик: Дмитрий (владелец Лидерры)
1. Контекст
Лидерра — реселлер лидов. Покупает поток номеров у поставщика crm.bp-gr.ru (бренд «BG») и перепродаёт своим тенантам через собственный портал.
У поставщика нет публичного REST API. Доступны:
- PUSH webhook для приёма лидов: настройка на /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_identifierdaily_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. Создание
- Клиент Лидерры в UI указывает
signal_type,signal_identifier,daily_limit,workdays. - Лидерра валидирует формат идентификатора:
- Сайт: валидный домен (regex
^[a-z0-9-]+(\.[a-z0-9-]+)+$нижний регистр) - Звонок: 11 цифр, начинается с
7 - СМС sender: 1–30 символов, буквы латиницы/цифры (до 6 цифр — короткий номер); 11-значные номера отвергаем (ограничение поставщика)
- СМС keyword: 1–60 символов, любые символы; опционально (если пусто — используется только B3)
- Сайт: валидный домен (regex
- Лидерра ищет существующий
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:
- Для каждого
supplier_projects— собирает все активные Лидерра-проекты, привязанные к нему. - Считает на день D+1:
total_quota[day]= суммаdaily_limitклиентов, у которых деньD+1вworkdaystarget_workdays= объединение всехworkdaysактивных клиентов
- Распределяет
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)
- Сайт / Звонок (3 платформы):
- Объединяет геофильтр:
regionsу поставщика = объединениеregionsвсех активных Лидерра-проектов на этом источнике;regions_reverseвсегдаfalse(включающий список). - Сравнивает с текущими настройками поставщика:
- Если изменилось — POST
rt-project-updateдля каждой из 3 платформ. - Если новый источник — POST
rt-project-save. - Если активных клиентов 0 — выключение/удаление.
- Если изменилось — POST
- Запас 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'а):
- Headless Playwright заходит на crm.bp-gr.ru.
- Если уже залогинен (по предыдущей cookie из Redis) — извлекает свежий CSRF-токен со страницы.
- Если нет — логинится с credentials из
.env(SUPPLIER_LOGIN,SUPPLIER_PASSWORD, защищены через Laravel encrypter). - Сохраняет
cookie(PHPSESSID) иcsrf_tokenв Redis с TTL 6 часов. - Основной sync-cron читает их оттуда и подмешивает в HTTP-запросы к
rt-*endpoints. - При 401/403 от поставщика — Лидерра запускает
RefreshSupplierSessionвнепланово.
Резерв (при сбоях):
- Очередь pending-changes: каждое неуспешное обновление помещается в очередь, повтор каждые 10 минут до 21:00.
- Алерт менеджеру: если очередь не пуста к 21:00 — уведомление в Telegram/email с инструкцией ручной правки через UI поставщика.
- Откат к предыдущему успешному состоянию: если новые настройки не дошли — клиенты Лидерры работают на вчерашних квотах (известное состояние, никакой потери).
- 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:
{
"vid": 432176649,
"project": "B1_vashinvestor.ru",
"tag": "Ваш инвестор",
"phone": "7XXXXXXXXXX",
"phones": ["7XXXXXXXXXX"],
"time": 1703781939
}
Обработчик (HandleSupplierWebhook):
- Проверяет защиту в два слоя:
- IP allowlist — запрос отвергается, если IP отправителя не из списка известных IP поставщика (хранится в
settings.supplier_ip_allowlist, обновляется админом). - Секретный токен в URL —
tenant_tokenэто длинный случайный секрет (≥32 символа), генерируется при регистрации интеграции; известен только Лидерре и поставщику. Неверный токен → 404 (не 403, чтобы не палить существование endpoint'а).
- IP allowlist — запрос отвергается, если IP отправителя не из списка известных IP поставщика (хранится в
- Парсит
project→ определяет(signal_type, signal_identifier):- префикс
B1_/B2_/B3_→ B-платформа - остальная часть → identifier
- префикс
- Сохраняет
supplier_lead(raw payload + метаданные). - Запускает
RouteLead(см. §6). - Возвращает 200 OK.
5.2. Резерв — CSV reconciliation
Раз в час (или 4 раза в день) Лидерра запрашивает CSV-экспорт /admin/report/index?type=49 (или archive endpoint) за последние сутки и сверяет:
- Список
vidиз CSV vssupplier_leadsв БД. - Если в CSV есть
vid, которого нет у нас — pending: восстанавливаем с тем жеRouteLeadflow (но помечаем какrecovered_from_csv). - Если расхождение > 5% за день — алерт менеджеру.
Это даёт стабильность даже при кратковременных сбоях нашего webhook'а.
6. Routing к клиентам Лидерры
При поступлении лида (RouteLead):
- Находим
supplier_projectsпо(signal_type, signal_identifier). - Определяем регион номера по коду телефона (справочник DEF-кодов).
- Берём список Лидерра-проектов на этом источнике с условиями:
status = 'active'- сегодняшний день в
workdays - не превышен дневной счётчик:
delivered_today < daily_limit - регион номера попадает в
regionsклиента (илиregionsпуст = все)
- Сортируем по
created_at ASC(старые проекты первыми — детерминированный порядок). - Для каждого подходящего проекта:
- Создаём
dealс независимой копией данных лида. - Инкрементируем
delivered_todayиdelivered_in_month. - Списываем с баланса по текущей ступени
pricing_tiers. - Шлём уведомление клиенту по его настройкам.
- Создаём
- Один и тот же лид → может попасть N клиентам (sharing).
- Если клиент уже исчерпал квоту — пропускаем.
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_project0 активных Лидерра-проектов — выключаем у поставщика, не удаляем сразу (теплый кэш на случай быстрого возврата).
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 | ✅ закрыт 10.05.2026 (см. §2.1, §3.1) | |
| 2 | ✅ закрыт 10.05.2026: фикс. (см. §7.1) | |
| 3 | ✅ закрыт 10.05.2026: 7-ступенчатый volume tier, месячный сброс (см. §7.2–7.4) | |
| 4 | ✅ закрыт 10.05.2026: MVP, объединение на supplier + фильтр на нашей стороне (см. §2.1, §4.1, §6) | |
| 5 | ✅ закрыт 10.05.2026: defense-in-depth (IP allowlist + секретный токен в URL, см. §5.1) | |
| 6 | ✅ закрыт 10.05.2026: headless Playwright cron каждый час + по 401/403 (см. §4.3) | |
| 7 | ✅ закрыт 10.05.2026: сразу выключение, удаление через 180 дней (см. §3.3) |
11. План реализации
После подтверждения дизайна — следующий шаг через skill writing-plans: декомпозиция на спринты, schema-миграции, контроллеры, jobs, тесты.