# Приёмка liderra.ru — ПУНКТ №9: Нехватка денег → откат + автопауза проекта + письмо

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

**Цель:** показать, что портал **никогда не уходит в минус**. Если у клиента не хватает денег на очередную заявку — заявка ему **не доставляется**, деньги **не списываются** (полный откат), его проект **автоматически встаёт на паузу**, и клиенту **уходит письмо** «баланс кончился, проект на паузе». При этом заявка спокойно уходит другим клиентам — поток не ломается.

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

**Источник сценария:** свод проверок №9 (M-INSUFF/M-BAL0); связка с PR3 (C4 = 600 ₽).

---

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

- Списание бросает **InsufficientBalanceException**, когда `balance_rub × 100 < цена` — [LedgerService.php:72-76](../../../app/app/Services/Billing/LedgerService.php#L72-L76).
- Это происходит **внутри транзакции доставки** — при исключении транзакция **откатывается**: сделка не создана, баланс не тронут — [RouteSupplierLeadJob.php:500-507](../../../app/app/Jobs/RouteSupplierLeadJob.php#L500-L507) (комментарий «Deal не создан, balance не тронут» — [:501](../../../app/app/Jobs/RouteSupplierLeadJob.php#L501)).
- **Автопауза + письмо** — `handleInsufficientBalance` [:521-548](../../../app/app/Jobs/RouteSupplierLeadJob.php#L521-L548):
  1. `UPDATE projects SET is_active=false` — проект встаёт на паузу — [:526-528](../../../app/app/Jobs/RouteSupplierLeadJob.php#L526-L528);
  2. письмо **ZeroBalancePausedMail** с лимитом **1 письмо в час на клиента** (Redis `Cache::add`/SETNX) — [:530-539](../../../app/app/Jobs/RouteSupplierLeadJob.php#L530-L539);
  3. предупреждение в лог — [:541-547](../../../app/app/Jobs/RouteSupplierLeadJob.php#L541-L547).
- После этого джоб возвращает `false` (без проброса) → **routing продолжает** разносить заявку остальным клиентам — [:502-506](../../../app/app/Jobs/RouteSupplierLeadJob.php#L502-L506).
- Письмо формирует [NotificationService::notifyZeroBalancePaused :122](../../../app/app/Services/NotificationService.php#L122); шаблон — [ZeroBalancePausedMail](../../../app/app/Mail/ZeroBalancePausedMail.php).

---

## Расклад на C4 (600 ₽)

- C4 баланс 600 ₽, счётчик месяца 0 → первая заявка по ступени T1 = **500 ₽**.
- 1-я заявка: списание 500 ₽ → баланс **100 ₽**, сделка создана.
- 2-я заявка: нужно 500 ₽, есть 100 ₽ → **нехватка** → откат (сделки нет, баланс остаётся 100 ₽) → проект C4 на паузу → письмо.

> Чтобы C4 гарантированно получил обе заявки, на прогоне настраиваем источник, где **C4 — получатель** (например его проект активен на тест-источнике, прочие на этом источнике на паузе), и вливаем 2 заявки.

---

## Шаг 9-0 — Готовое предусловие: C4 = 600 ₽, проект активен

**🔧 Код:** баланс — `tenants.balance_rub`; проект — `projects.is_active`.
**🎬 Действие (лок):** C4 баланс 600 ₽ (из PR3), его проект `is_active=true`, счётчик месяца 0. Зафиксировать «до».

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Баланс C4 | — | 600 ₽ | _(слот прогона)_ | 📸 Дашборд C4 (600 ₽) |
| Проект C4 | — | активен (ловит заявки) | _(слот)_ | 📸 Проекты C4 (активен) |

**❌ Если не так:** баланс ≠ 600 / проект на паузе заранее → подготовить заново (PR3).

---

## Шаг 9-1 — 1-я заявка проходит (списание 500 ₽, баланс 600→100 ₽)

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

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Баланс C4 | 600 ₽ | **100 ₽** (списано 500 ₽) | _(слот)_ | 📸 Дашборд/Биллинг (100 ₽) |
| Сделка | нет | 1 сделка появилась | _(слот)_ | 📸 Сделки C4 (1 шт) |

**❌ Если не так:** баланс ≠ 100 → ошибка цены/ступени.

---

## Шаг 9-2 — 2-я заявка: нехватка → откат + пауза + письмо

**🔧 Код:** нехватка/откат — [LedgerService :72-76](../../../app/app/Services/Billing/LedgerService.php#L72-L76) + [RouteSupplierLeadJob :500-507](../../../app/app/Jobs/RouteSupplierLeadJob.php#L500-L507); автопауза+письмо — [:521-548](../../../app/app/Jobs/RouteSupplierLeadJob.php#L521-L548).
**🎬 Действие (лок):** влить C4 вторую заявку.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Баланс C4 | 100 ₽ | **остался 100 ₽** (НЕ ушёл в минус, не списано) | _(слот)_ | 📸 Дашборд/Биллинг (по-прежнему 100 ₽) |
| Вторая сделка | — | **не создана** (заявку не доставили) | _(слот)_ | 📸 Сделки C4 (по-прежнему 1 шт) |
| Проект C4 | активен | **встал на паузу** автоматически | _(слот)_ | 📸 Проекты C4 (на паузе) |
| Письмо клиенту | — | пришло «баланс кончился, проект на паузе» (1/час) | _(слот)_ | 📸 письмо (локально — почтовый просмотрщик/лог); 📝 факт отправки |

**💡 Что внутри:** на вторую заявку у C4 не хватило денег (надо 500 ₽, осталось 100 ₽). Портал не стал доставлять её в долг: заявку клиенту не отдал, деньги не тронул (баланс как был — 100 ₽), а проект сам поставил на паузу, чтобы клиент не получал то, за что не может заплатить. И сразу отправил письмо-предупреждение. Когда клиент пополнит баланс — снимет паузу и продолжит. В минус уйти невозможно.

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

---

## Шаг 9-3 — Заявка не потеряна: уходит другим клиентам

**🔧 Код:** возврат `false` без проброса → routing продолжается — [RouteSupplierLeadJob :502-506](../../../app/app/Jobs/RouteSupplierLeadJob.php#L502-L506).
**🎬 Действие (лок):** при наличии других подходящих клиентов на источнике — проверить, что та же заявка доставлена им.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Доставка другим | — | заявка ушла другим подходящим клиентам (поток не сломан) | _(слот)_ | 📸 Сделки другого клиента (заявка есть) |

**❌ Если не так:** заявка пропала у всех из-за нехватки у одного → дефект (нехватка одного не должна ронять раздачу).

---

## Шаг 9-4 — Вечерняя заморозка по завтрашней ёмкости (§20.2, cover-freeze)

Это **НЕ** мгновенная пауза 9-2 (она при нехватке прямо в раздаче, §19.5). Вечерний sweep смотрит на **ЗАВТРА**: тенант может замёрзнуть **с положительным балансом**, если его не хватит на завтрашние лимиты.

**🔧 Код:** `BalancePreflightSweepJob` @18:00 МСК (`billing:preflight-sweep`, [routes/console.php:82-83](../../../app/routes/console.php#L82)); `required = requiredLeadsForTomorrow()` — [:62](../../../app/app/Jobs/Billing/BalancePreflightSweepJob.php#L62); freeze → `frozen_by_balance_at=now()` + пауза проектов — [:70-85](../../../app/app/Jobs/Billing/BalancePreflightSweepJob.php#L70); разморозка снимает паузу **только** у `paused_at >= frozen_at_was` (наши) — **ручные паузы клиента (`paused_at < frozen_at_was`) СОХРАНЯЮТСЯ** — [:97-108](../../../app/app/Jobs/Billing/BalancePreflightSweepJob.php#L97). Письмо-напоминание о заморозке — `billing:frozen-reminder` @18:30.

**🎬 Действие (прод/лок):** тенанту с положительным балансом, которого НЕ хватит на завтрашние лимиты, прогнать `php artisan billing:preflight-sweep` (бэкдейт/подгон не нужен — sweep сам смотрит завтра). Затем поставить один проект на **ручную** паузу, пополнить баланс, снова прогнать sweep.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Вечерняя заморозка | баланс «+», но мало на завтра | тенант заморожен (`frozen_by_balance_at` ≠ null), проекты на паузе | _(слот)_ | 📝 `frozen_by_balance_at`, 📸 проекты на паузе |
| Письмо о заморозке | — | пришло (18:30, `billing:frozen-reminder`) | _(слот)_ | 📸/📝 письмо |
| Разморозка не трогает ручную паузу | 1 проект на ручной паузе ДО заморозки | после пополнения+sweep: наши паузы сняты, **ручная пауза осталась** | _(слот)_ | 📝 `paused_at` ручного проекта НЕ обнулён |

**❌ Если не так:** заморозка не сработала по завтрашней ёмкости / разморозка сняла **ручную** паузу клиента (затёрла его решение) → дефект §20.2.

---

## Verification №9 (выход из пункта)
- [ ] 📸 Баланс C4 после нехватки = **100 ₽** (не минус, не списано).
- [ ] 📸 Сделок у C4 ровно **1** (вторая не создана).
- [ ] 📸 Проект C4 **на паузе**.
- [ ] 📸/📝 Письмо «баланс кончился, проект на паузе» отправлено (1/час/клиент).
- [ ] 📸 Заявка доставлена другому подходящему клиенту (не потеряна).
- [ ] 📝 Вечерняя заморозка (§20.2): тенант с «+» балансом, но мало на завтра → заморожен; разморозка после пополнения **не трогает ручную паузу** клиента.

## 📸 vs 📝 в этом пункте
- **📸 Скриншот:** Баланс/Дашборд C4, Сделки C4 (1 шт), Проекты C4 (пауза), письмо (если есть просмотрщик), Сделки другого клиента.
- **📝 Текст:** факт отката транзакции, rate-limit письма 1/час (Redis), запись в лог — глазами не видны.

## Грабли
- Лимит письма **1/час/клиент** — при повторных нехватках в тот же час второго письма не будет (это правильно, не баг).
- Чтобы C4 получил именно 2 заявки, настроить источник так, чтобы C4 был получателем (прочие на источнике — на паузе/недоступны).
- Автопауза снимается только пополнением+ручным включением — это ожидаемо, не дефект.
- Локально письмо — в почтовом просмотрщике/логе очереди; на проде — реальный ящик клиента.
