# Приёмка liderra.ru — ПУНКТ №11: Лента денег = база + калькуляторы («хватит на… дней»)

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

**Цель:** показать, что кошелёк клиента честный: число в кошельке = тому, что в базе; история списаний = реальным операциям, остаток в ней **только уменьшается** (монотонно, без скачков вверх без причины); подсказка «хватит на… дней» считается **правильно** и совпадает между Дашбордом и Биллингом; выгрузку истории в файл (CSV) можно скачать, и она = тому, что на экране.

**Что снимаем глазами:** **Биллинг** (кошелёк + лента списаний с остатком), **Дашборд** («хватит на дней»), файл выгрузки. Живые 📸.

**Источник сценария:** свод проверок №11 (M-LEDGER/M-CALC); R3 «движок».

---

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

- **Кошелёк** — `GET /api/billing/wallet`: `balance_rub` + конверсия в лиды — [BillingController.php:78-103](../../../app/app/Http/Controllers/Api/BillingController.php#L78-L103).
- **История денег** — `GET /api/billing/transactions`: пагинированная лента `balance_transactions` (20/стр), у каждой строки `display_amount_rub` и остаток после — [:223-241](../../../app/app/Http/Controllers/Api/BillingController.php#L223-L241).
- **Список списаний** — `GET /api/billing/charges`, сортировка `charged_at desc` — [TenantChargesController::index :37-46](../../../app/app/Http/Controllers/Api/TenantChargesController.php#L37-L46).
- **Выгрузка CSV списаний** — `POST /api/billing/charges/export`, потоком + чанками, колонки `charged_at, deal_id, tier_no, charge_source, price_rub, balance_rub_after` (остаток после) — [export :63-147](../../../app/app/Http/Controllers/Api/TenantChargesController.php#L63-L147), заголовок — [:85](../../../app/app/Http/Controllers/Api/TenantChargesController.php#L85).
- **«Хватит на… дней»** — `RunwayCalculator::daysLeft(tenant, affordableLeads)`; affordableLeads = ёмкость баланса в лидах по тарифу — [DashboardController :122-124](../../../app/app/Http/Controllers/Api/DashboardController.php#L122-L124). **Единый источник с биллингом** (фикс F3 17.06) — [:110](../../../app/app/Http/Controllers/Api/DashboardController.php#L110).
- **Ёмкость баланса в лидах** — `BalanceToLeadsConverter` (по ступеням, bcmath) — [:86-87](../../../app/app/Services/Billing/BalanceToLeadsConverter.php#L86-L87); статус/дефицит — [BillingController::balanceStatus :125-161](../../../app/app/Http/Controllers/Api/BillingController.php#L125-L161).

---

## Шаг 11-1 — Кошелёк = база

**🔧 Код:** [BillingController::wallet :78-103](../../../app/app/Http/Controllers/Api/BillingController.php#L78-L103).
**🎬 Действие (лок):** открыть Биллинг клиента; сверить число кошелька с базой.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Кошелёк на экране | — | = `balance_rub` в базе (до копейки) | _(слот прогона)_ | 📸 Биллинг (кошелёк); 📝 SQL `balance_rub` |

**❌ Если не так:** экран ≠ база → дефект отображения денег.

---

## Шаг 11-2 — Лента списаний = операциям, остаток только уменьшается

**🔧 Код:** [BillingController::transactions :223-241](../../../app/app/Http/Controllers/Api/BillingController.php#L223-L241); список — [TenantChargesController::index :37-46](../../../app/app/Http/Controllers/Api/TenantChargesController.php#L37-L46).
**🎬 Действие (лок):** открыть историю списаний; сверить число строк и проследить остаток сверху вниз.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Число строк | — | = числу операций в базе | _(слот)_ | 📸 лента; 📝 SQL `COUNT balance_transactions` |
| Суммы | — | каждая = реальному списанию (цена тарифа) | _(слот)_ | 📸 строки списаний |
| Остаток после | — | **монотонно уменьшается** по ходу списаний (вверх — только при пополнении) | _(слот)_ | 📸 колонка остатка; 📝 `balance_rub_after` убывает |

**💡 Что внутри:** в истории видно каждое движение денег: сколько списали и сколько осталось после. Остаток идёт ровно вниз — на каждое списание уменьшается на цену заявки. Вверх он прыгает только когда клиент пополняет счёт. Никаких необъяснимых скачков — историю можно сверить копейка в копейку.

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

---

## Шаг 11-3 — «Хватит на… дней» считается верно и одинаково везде

**🔧 Код:** [RunwayCalculator :122-124](../../../app/app/Http/Controllers/Api/DashboardController.php#L122-L124); единый источник — [:110](../../../app/app/Http/Controllers/Api/DashboardController.php#L110); ёмкость — [BalanceToLeadsConverter :86-87](../../../app/app/Services/Billing/BalanceToLeadsConverter.php#L86-L87).
**🎬 Действие (лок):** сверить «хватит на дней» на Дашборде и в Биллинге у одного клиента.

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

**💡 Что внутри:** портал подсказывает клиенту, на сколько ещё дней хватит денег — считая по его балансу и темпу получения заявок. Раньше эта цифра на двух экранах могла различаться (находка F3) — теперь источник один, и Дашборд с Биллингом показывают одно и то же.

**❌ Если не так:** Дашборд ≠ Биллинг по «хватит на дней» (рецидив F3) / ёмкость неверная → дефект калькулятора.

---

## Шаг 11-4 — Выгрузка истории в CSV = экрану

**🔧 Код:** [TenantChargesController::export :63-147](../../../app/app/Http/Controllers/Api/TenantChargesController.php#L63-L147).
**🎬 Действие (лок):** скачать CSV списаний; сверить с лентой на экране.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Строки в файле | — | = ленте (дата, сделка, ступень, цена, остаток после) | _(слот)_ | 📸 файл + сверка с экраном |
| Только свои | — | чужих списаний нет | _(слот)_ | 📝 все строки своего тенанта |

**❌ Если не так:** в файле чужое / расходится с экраном → дефект.

> ⚠️ Этот CSV пишется через `fputcsv` (как и экспорт сделок №14) — **та же** оговорка про CSV-инъекцию: проверить на прогоне, нейтрализуются ли ведущие `= + - @` (см. №14 Шаг 14-4).

---

## Verification №11 (выход из пункта)
- [ ] 📸 Кошелёк = база (до копейки).
- [ ] 📸 Лента списаний = операциям; остаток монотонно уменьшается.
- [ ] 📸 «Хватит на дней» Дашборд = Биллинг (единый источник, F3 не рецидивит).
- [ ] 📸 CSV списаний = экрану; только свои.

## 📸 vs 📝 в этом пункте
- **📸 Скриншот:** Биллинг (кошелёк, лента, остаток), Дашборд («хватит на дней»), файл CSV.
- **📝 Текст:** `COUNT`/`balance_rub_after`/принадлежность тенанту — частично текстом.

## Грабли
- F3 (расхождение «хватит на дней» Дашборд↔Биллинг) — теперь единый источник; на прогоне явно сверить оба экрана.
- Остаток вверх — только при пополнении (topup); любой иной рост — дефект.
- CSV-инъекция — общая с №14, отдельно подтвердить файлом.
