Files
portal/docs/superpowers/specs/2026-05-10-supplier-integration-design.md
T
Дмитрий 3f463f48ea docs(specs): close all 7 open questions in supplier integration design
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>
2026-05-10 11:39:29 +03:00

21 KiB
Raw Blame History

Дизайн интеграции Лидерра ↔ Поставщик 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_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:

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

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

  1. Проверяет защиту в два слоя:
    • IP allowlist — запрос отвергается, если IP отправителя не из списка известных IP поставщика (хранится в settings.supplier_ip_allowlist, обновляется админом).
    • Секретный токен в URLtenant_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, тесты.