Архитектура affiliate-модуля под Obmenka CRM

Источник: Plan агент с чтением existing code, 2026-05-16

TL;DR

  1. Новый домен Affiliate — отдельный тип пользователя с ref-кодом и USDT-балансом
  2. Client.referrer_affiliate_id — denorm указатель, client_affiliations — audit history
  3. commission_entries: одна запись на (deal_id, affiliate_id, tier), UNIQUE для идемпотентности
  4. Multi-level через parent_affiliate_id (≤ 3 уровня) + denorm tier2/3_parent_id
  5. Trigger расчёта = переход Deal.status → done (в той же транзакции что сделка)
  6. Lifecycle: pending → available (после hold) → paid (через payout_request → operation)
  7. RBAC новая роль affiliate с отдельным layout aff_base.html
  8. MVP = single-level, плоская комиссия, ручные выплаты

Модель данных

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

  • affiliate_programs (id, code, name, base_model, tier1/2/3_pct, hold_days, min_payout, attribution_mode...)
  • affiliates (id, user_id, external_ref, parent_affiliate_id, tier2/3_parent_id denorm, override ставки, payout_method, status)
  • ref_codes (id, affiliate_id, code, label, clicks/signups/deals counters)
  • client_affiliations (id, client_id, affiliate_id, ref_code_id, source, attached_at, detached_at) — partial UNIQUE на одну активную
  • commission_entries (id, deal_id, affiliate_id, tier, program_id, base_value_usdt, rate_pct, amount_usdt, status, available_at, paid_at, reverses_entry_id) — UNIQUE(deal_id, affiliate_id, tier)
  • payout_requests (id, affiliate_id, amount, status, payout_op_id, matched_raw_tx_id)

Изменения

  • clients: +referrer_affiliate_id, +referrer_ref_code_id, +referrer_set_at
  • deals: +affiliate_snapshot_id, +commission_calculated_at
  • users: +totp_secret, +totp_enabled, +affiliate_id
  • roles: новая запись affiliate

Альтернативы отвергнуты

  • Materialized view — у комиссий lifecycle, нужны транзакции
  • Граф через CTE без denorm — не работает CHECK от циклов
  • Только history без denorm referrer — будет JOIN на каждом списке

Алгоритм расчёта

def accrue_for_deal(deal):
    if not deal.client.referrer_affiliate_id: return
    aff = load(referrer_affiliate_id)
    if aff.status != 'active': return
    caff = active_affiliation(deal.client_id)
    if caff.expires_at and deal.occurred_at > caff.expires_at: return
    if caff.attribution_mode == 'first_deal_only' and deals_count > 1: return

    base = compute_base(deal, program.base_model)  # volume/profit/fixed/hybrid

    # Tier 1
    rate1 = aff.override_tier1_pct or program.tier1_pct
    amt1 = base * rate1 + program.fixed_usdt_per_deal
    if program.max_commission_per_deal_usdt: amt1 = min(amt1, ...)
    upsert(deal_id, aff.id, tier=1, amount=amt1)

    # Tier 2 (если parent есть)
    # Tier 3 (если tier3_parent_id есть)

    deal.affiliate_snapshot_id = aff.id

Edge cases

  • aff.blocked → не начисляем, upline тоже молчит (анти-invite-fraud)
  • cancel deal → reverse_entry с -amount
  • recalc после edit → admin-only command, voidает pending + считает заново
  • перепривязка клиента → старые комиссии за старым партнёром (snapshot)
  • self-referral → check tg_username/phone match → void
  • cycle в parent → защищено CHECK + BFS при назначении

Tracking

3 пути привязки клиента к партнёру: 1. Через ссылку /r/{code} → cookie 30 дней → при создании Client 2. Manual менеджером в карточке клиента 3. Self-claim через TG-бот (фаза 3)

/r/{code} — публичный endpoint, инкремент counter, signed cookie через URLSafeTimedSerializer, 302 на main page.

Перепривязка: detached_at = now() на старой, новая активная. Старые комиссии не трогаются.

Lifecycle комиссии и выплат

accrue → pending
   ↓ (hold_days)
available
   ↓ partner requests payout, admin approves
paid (linked to Operation crypto_out)
   ↓ reconcile.py связывает с raw_blockchain_tx
matched_raw_tx_id заполнен

Балансы партнёра = SUM(amount) WHERE status='pending'/'available'/'paid'.

Hold-cron каждый час: pending → available WHERE created_at + hold_days <= NOW() AND NOT (deal status cancelled).

RBAC

Роль Может
admin CRUD programs, CRUD affiliates, approve payouts, корректировка commission
manager Привязка клиента к партнёру (с reason), просмотр
accountant Read-only + payouts approve
cashier Только имя партнёра в карточке клиента
viewer Без affiliate-инфы
affiliate Свой кабинет: ссылки, клиенты (псевдонимы), заработок, выплаты, settings

Privacy: партнёр НЕ видит ФИО/телефон клиентов, только external_ref C-XXXX + агрегаты.

2FA: TOTP в фазе 7.2 (опц на MVP, обязательно для production-выхода).

API endpoints

Публичные: - GET /r/{code} — tracking redirect

Партнёрский кабинет (/aff/*, требует role=affiliate): - Dashboard, links, clients, earnings, payouts, settings

Админ (/affiliates/*, /affiliate-programs/*, /payouts/*): - CRUD affiliates, programs, attach клиента, approve payouts, void/recalc commission

Internal jobs: - python -m app.jobs.commission_release (hourly) - python -m app.jobs.commission_payout_auto (фаза 2)

UI

Админ — под существующим сайдбаром, новая группа «Партнёры»: - /affiliates — список - /affiliates/{id} — карточка с tabs (обзор/клиенты/комиссии/выплаты/даунлайн/настройки) - /affiliate-programs — программы - /payouts — pending выплаты

Изменения существующих: - client_card.html — блок «Привязка к партнёру» - clients_list.html — колонка «Партнёр» - deal_card.html — блок «Комиссии партнёрам» - dashboard.html — виджет аффилиатной программы

Партнёрский кабинетотдельный layout aff_base.html (отличается визуально): - /aff дашборд - /aff/links мои ссылки - /aff/clients мои клиенты (псевдонимизированно) - /aff/earnings заработок (с экспортом CSV) - /aff/payouts выплаты - /aff/settings

Фазы

E7.1 MVP (3–4 нед)

  • Миграция таблиц
  • 1 модель: volume_usdt с фиксированным %
  • БЕЗ multi-level
  • attribution = lifetime
  • Кабинет (без 2FA)
  • Tracking /r/{code} + cookie + auto-attach
  • Manual attach менеджером
  • Accrual в deal_change_status и deal_new_post
  • Hold-cron
  • Admin: CRUD + approve payouts

E7.2 (2–3 нед)

  • Multi-level + tier2/3
  • Override-ставки
  • attribution_mode варианты
  • Auto payout по threshold
  • Scheduled payouts
  • 2FA TOTP
  • Anti-self-referral
  • UI дерево даунлайна

E7.3 (отложено)

  • Smart-contract выплаты
  • ML/rule fraud detection
  • Отдельный subdomain partners.obmenka...
  • Webhooks партнёрам
  • A/B-тесты программ
  • Tax reporting

Риски

  1. Цикл parent_affiliate_id → защита: CHECK + BFS при назначении + MAX_TIER_DEPTH константа
  2. Hold-cron падение → APScheduler мониторинг + admin страница «холд должен был истечь»
  3. Race при accrue → UNIQUE (deal_id, affiliate_id, tier) защищает
  4. Изменение ставки задним числом → snapshot ставки в commission_entries → не меняется
  5. Cancel после выплаты → reverse-entry с negative, баланс уйдёт в минус, ручной разбор

Производительность

При 150 сделок/мес = 5 в день → ms. Реалистичный max 10k/мес = 30k entries/год — все запросы покрыты индексами: - (affiliate_id, status) для балансов - (deal_id) для лукапа - partial available_at WHERE status='pending' для cron - clients.referrer_affiliate_id для списка клиентов партнёра

Чего НЕ делаем в E7.1

Что Почему
Multi-level Нет partners для этого
Auto-payouts Нужно окно для проверки fraud
2FA Доверенные на старте
Self-signup Barrier против fraud-аккаунтов
Несколько программ Одна default достаточно
Smart-contract Хранение приватных ключей — отдельный домен
Subdomain Caddy complexity
Webhooks Нет инфры