# Приёмка liderra.ru — ПУНКТ №1: Распределение / CAP=3 (одна заявка → не больше 3 клиентов)

> **Для исполнителя:** формат PR1 (одобрен 20.06). Каждый шаг = `🔧 Код-факт (file:line)` · `🎬 Действие` · `📋 ОТЧЁТ (было→ожидали→стало + 📸/📝)` · `❌ Если не так`. Техника «под капотом»; владельцу — человеческие карточки.

**Цель:** показать, что одна входящая заявка раздаётся честно и с защитой: её получают **не больше 3 клиентов**, при этом **по одному разу каждый** (один и тот же клиент не получит её дважды), и только те, у кого **есть деньги** и **не выбран дневной лимит**. Это защищает и владельца номера (заявку продают максимум 3 раза), и клиентов (никто не получает сверх лимита).

**Что снимаем глазами:** раздел **Сделки** у получателей (одна заявка → 3 сделки в 3 разных кабинетах) и **Баланс** (списание у каждого из 3). Живые 📸.

**Источник сценария:** свод проверок №1 (D-CAP); R3 «движок».

---

## 🔧 Код-факты (подтверждено чтением 20.06)

- **CAP = 3** — заявка продаётся максимум 3 раза — [LeadDistributor.php:22](../../../app/app/Services/LeadDistributor.php#L22); отбор ≤3 случайных из подходящих — [:32-43](../../../app/app/Services/LeadDistributor.php#L32-L43).
- **По одному на клиента** — `SELECT DISTINCT ON (snap.tenant_id)` — [LeadRouter.php:137](../../../app/app/Services/LeadRouter.php#L137) (на клиента берётся один проект, не несколько).
- **Дневной лимит не превышается** — в кандидаты попадают только проекты, где `projects.delivered_today < snap.daily_limit` — [:145](../../../app/app/Services/LeadRouter.php#L145).
- **Только платёжеспособные** — клиент в отборе, если `balance_rub > 0` и не заморожен (`frozen_by_balance_at IS NULL`) — [:146-151](../../../app/app/Services/LeadRouter.php#L146-L151).
- Вероятность попасть ∝ **остатку лимита** (у кого больше свободных слотов — выше шанс), мелкие не отрезаются — [weightedPick :171](../../../app/app/Services/LeadRouter.php#L171).
- CAP применяется **поверх** каскада региона (см. пункт №3): сначала лесенка региона, затем ≤3 получателя.

---

## Шаг 1-0 — Предусловие: 7 клиентов ловят один источник

**🔧 Код:** кандидаты — [LeadRouter::queryCandidates :104-160](../../../app/app/Services/LeadRouter.php#L104-L160).
**🎬 Действие (лок):** 7 клиентов (балансы 100 000 ₽), у каждого проект на одном источнике, регион подходит, лимит не выбран. (Это ровно состояние засева `imitation:seed --clients=7`.)

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Подходящих клиентов | — | 7 (все активны, с деньгами, лимит свободен) | _(слот прогона)_ | 📝 SQL: 7 проектов с `delivered_today<limit`, баланс>0 |

**❌ Если не так:** меньше 7 подходящих → проверить балансы/лимиты/активность.

---

## Шаг 1-1 — Влить ОДНУ заявку → ровно ≤3 получателя, разные клиенты

**🔧 Код:** [LeadDistributor :32-43](../../../app/app/Services/LeadDistributor.php#L32-L43) + DISTINCT ON [LeadRouter :137](../../../app/app/Services/LeadRouter.php#L137).
**🎬 Действие (лок):** влить **1** заявку на источник, где подходят все 7.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Сколько клиентов получили | — | **ровно 3** (не 7, не 4) | _(слот)_ | 📸 Сделки 3 клиентов (у каждого 1 новая) |
| Один клиент дважды | — | **нет** — у каждого получателя 1 сделка | _(слот)_ | 📸 Сделки получателей (по 1 шт) |
| Остальные 4 | — | заявку не получили | _(слот)_ | 📸 Сделки одного из «не выбранных» (без новой) |

**💡 Что внутри:** заявка пришла одна, а желающих её получить — 7. Портал отдаёт её не всем подряд, а максимум трём, выбранным честным жребием (у кого больше свободных слотов — выше шанс). И каждому из троих — ровно один раз. Так заявку не «размножают» сверх меры: владелец номера-донора защищён, его контакт продают максимум 3 раза.

**❌ Если не так:** получили >3 / один клиент 2 раза / получили те, у кого нет денег → дефект распределения.

---

## Шаг 1-2 — У каждого из 3 — списание ровно один раз

**🔧 Код:** списание на доставку — [LedgerService :53-95](../../../app/app/Services/Billing/LedgerService.php#L53-L95).
**🎬 Действие:** открыть Баланс/Биллинг троих получателей.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Списание у получателей | — | у каждого из 3 — одно списание по цене тарифа | _(слот)_ | 📸 Биллинг 3 клиентов (по 1 списанию) |
| Списание у «не выбранных» | — | нет | _(слот)_ | 📸 Биллинг одного «не выбранного» (без списания) |

**❌ Если не так:** списание без сделки / двойное списание / списание у не получивших → денежный дефект.

---

## Шаг 1-3 — Дневной лимит не превышается

**🔧 Код:** фильтр лимита — [LeadRouter :145](../../../app/app/Services/LeadRouter.php#L145); счётчик `delivered_today` растёт при доставке.
**🎬 Действие (лок):** выбрать клиента с маленьким дневным лимитом (например 2), влить поток заявок ему на источник; проверить, что после 2 доставок он больше не получает.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Доставки клиенту с лимитом 2 | 0 | ровно 2, дальше — нет | _(слот)_ | 📸 Сделки клиента (2 шт, не растёт) |
| Счётчик дня | — | `delivered_today` не превышает лимит | _(слот)_ | 📝 SQL `delivered_today ≤ daily_limit` |

**❌ Если не так:** доставок больше лимита → дефект (клиент получает сверх оплаченного объёма).

---

## Verification №1 (выход из пункта)
- [ ] 📸 Одна заявка → ровно 3 получателя, 3 разных клиента, у каждого 1 сделка.
- [ ] 📸 Списание у каждого из 3 (по разу); у «не выбранных» — нет.
- [ ] 📸 Клиент с лимитом 2 получает ровно 2, дальше не растёт.
- [ ] 📝 SQL: на одного клиента не больше одной сделки на заявку; `delivered_today ≤ daily_limit`.

## 📸 vs 📝 в этом пункте
- **📸 Скриншот:** Сделки получателей (3 кабинета) и «не выбранного»; Биллинг (списания); Сделки клиента у лимита.
- **📝 Текст:** число eligible, `delivered_today`, жребий — глазами не видны.

## Грабли
- Жребий случайный — конкретные 3 клиента меняются от прогона к прогону; проверяем **число** (≤3) и «по одному на клиента», а не конкретные имена.
- CAP идёт **после** каскада региона (№3): если в фазе 1 уже 3 — фаза 2/3 не добавляет.
- Заморозка/нулевой баланс выкидывают клиента из отбора (связка с №9).
