# Приёмка liderra.ru — ПУНКТ №13: Сделки — лента / карточка / статусы (воронка)

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

**Цель:** показать, что раздел **Сделки** работает честно: число сделок совпадает с тем, что в базе; в карточке сделки видна **реальная цена** заявки (не ноль); сделку можно вести по **воронке статусов** (новая → в работе → выиграна/проиграна), и смена статуса **не списывает деньги повторно** — деньги берутся один раз при доставке, а движение по воронке бесплатно.

**Что снимаем глазами:** **Сделки** (список и фильтры), **карточка сделки** (цена, статус), смена статуса в воронке, **Баланс** (не меняется при смене статуса). Живые 📸.

**Источник сценария:** свод проверок №13 (T-LIST/CARD/STATUS); R3 «движок».

---

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

- **Список сделок** — `GET /api/deals` с фильтрами `status_in/project_id/manager_id/search/даты`, свежие сверху (`received_at DESC`), keyset-пагинация — [DealController::index :57-210](../../../app/app/Http/Controllers/Api/DealController.php#L57-L210). Число = `COUNT(*)` из базы — [:168](../../../app/app/Http/Controllers/Api/DealController.php#L168); режим `count_only` — только число — [:139-141](../../../app/app/Http/Controllers/Api/DealController.php#L139-L141).
- **Карточка сделки** — `GET /api/deals/{id}` показывает статус, события и **реальную стоимость лида** из снимка списания `lead_charges` — [show :236-311](../../../app/app/Http/Controllers/Api/DealController.php#L236-L311); цена = `price_per_lead_kopecks`, если списание было в рублях — [:311](../../../app/app/Http/Controllers/Api/DealController.php#L311).
- **Смена статуса (воронка)** — `update` валидирует, что slug есть в таблице `lead_statuses` — [:358-363](../../../app/app/Http/Controllers/Api/DealController.php#L358-L363); меняет `deal.status` и пишет событие `deal.status_changed` (`from/to/source='manual'`) — [:425-433](../../../app/app/Http/Controllers/Api/DealController.php#L425-L433). **Списание (LedgerService) при смене статуса НЕ вызывается** — статус меняется без денег.
- Статусы воронки — из таблицы `lead_statuses` (настраиваемый набор), не зашиты в код.
- ⚠️ **Порядок воронки НЕ форсится (N-6):** API принимает **любой валидный slug в любом порядке** — state-machine запрещённых/обратных переходов в коде НЕТ (`won→new` пройдёт 200). `StatusRuToSlugMapper` — только для CSV-импорта (RU-метка→slug), API-путь его не зовёт. **Не искать** блокировку обратных переходов — ожидание = «любой валидный slug, воронка не форсится».

---

## Шаг 13-1 — Лента сделок: число = базе, фильтры, свежие сверху

**🔧 Код:** [DealController::index :57-210](../../../app/app/Http/Controllers/Api/DealController.php#L57-L210).
**🎬 Действие (лок):** открыть Сделки клиента, у которого известно число сделок (например 10 из засева); применить фильтр по статусу/проекту.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Число сделок в ленте | — | совпадает с базой (напр. 10) | _(слот прогона)_ | 📸 Сделки (счётчик/список); 📝 SQL `COUNT(*) deals WHERE tenant_id=…` |
| Порядок | — | свежие сверху (по дате) | _(слот)_ | 📸 верх списка — последняя заявка |
| Фильтр по статусу/проекту | — | список сужается верно | _(слот)_ | 📸 список после фильтра |

**❌ Если не так:** число ≠ базе / порядок не по дате / фильтр врёт → дефект ленты.

---

## Шаг 13-2 — Карточка сделки: видна реальная цена (не ноль)

**🔧 Код:** [show :236-311](../../../app/app/Http/Controllers/Api/DealController.php#L236-L311); цена из `lead_charges` — [:311](../../../app/app/Http/Controllers/Api/DealController.php#L311).
**🎬 Действие (лок):** открыть карточку одной из доставленных (платных) сделок.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Цена лида в карточке | — | **реальная цена списания** (напр. 500 ₽), не 0 | _(слот)_ | 📸 карточка сделки (стоимость) |
| Статус | — | текущий статус показан | _(слот)_ | 📸 карточка (статус) |
| Телефон/город/проект | — | данные заявки на месте | _(слот)_ | 📸 карточка |

**❌ Если не так:** «Стоимость лида 0 ₽» при реальном списании 500 ₽ → дефект отображения (известная находка F2 — проверить, что цена тянется из `lead_charges`).

---

## Шаг 13-3 — Воронка статусов: смена не списывает деньги повторно

**🔧 Код:** смена статуса без списания — [update :347-456](../../../app/app/Http/Controllers/Api/DealController.php#L347-L456).
**🎬 Действие (лок):** перевести сделку по воронке (например «новая» → «в работе» → «выиграна»); зафиксировать баланс «до» и «после».

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Статус сделки | новая | сменился на «в работе»/«выиграна» | _(слот)_ | 📸 карточка/лента до и после |
| Баланс клиента | X ₽ | **не изменился** (статус ≠ деньги) | _(слот)_ | 📸 Баланс/Биллинг до = после |
| Новых списаний | — | нет (движение по воронке бесплатно) | _(слот)_ | 📝 SQL: число `lead_charges` не выросло |
| История | — | записано событие смены статуса | _(слот)_ | 📝 событие `deal.status_changed` |

**💡 Что внутри:** деньги за заявку портал берёт **один раз** — в момент доставки. Дальше клиент ведёт сделку по этапам (новая → в работе → выиграна или проиграна) — это его рабочий процесс, и он **бесплатный**. Даже когда сделка помечается «выиграна», повторного списания не происходит. Платят за получение заявки, а не за её статус.

**❌ Если не так:** смена статуса (особенно на «выиграна») списала деньги ещё раз → денежный дефект (СТОП).

---

## Verification №13 (выход из пункта)
- [ ] 📸 Лента: число сделок = базе; свежие сверху; фильтры работают.
- [ ] 📸 Карточка: реальная цена лида (не 0), статус, данные заявки.
- [ ] 📸 Смена статуса по воронке проходит; баланс до = после.
- [ ] 📝 При смене статуса число `lead_charges` не растёт; записано событие смены.

## 📸 vs 📝 в этом пункте
- **📸 Скриншот:** Сделки (лента, фильтры), карточка сделки (цена, статус), баланс до/после смены статуса.
- **📝 Текст:** `COUNT(*)` сделок, число `lead_charges`, событие `deal.status_changed` — частично текстом.

## Грабли
- Цена в карточке тянется из снимка `lead_charges` — если показан 0 при реальном списании, это дефект отображения (F2), а не отсутствие денег.
- Статусы — из таблицы `lead_statuses`; набор может отличаться от «new/won/lost», сверять с реальными slug'ами на прогоне.
- Ручные/массовые действия и экспорт — отдельный пункт №14; удаление сделки не возвращает деньги — там же.
