# Архитектура 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 на каждом списке ## Алгоритм расчёта ```python 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 | Нет инфры |