# 08 · Биллинг

[← Home](Home.md)

Деньги — самая чувствительная часть домовой бухгалтерии. Здесь: как устроена
модель, как начислять, принимать оплаты и делать перерасчёты так, чтобы ни
копейка не потерялась и никто не заплатил дважды.

## Модель данных

```
Charge (начисление)            — «15 000 ₽ за Октябрь 2026, 20 участников»
  └── Invoice (счёт) × N       — по одному на участника: сумма, срок, статус
        └── Payment (оплата)   — деньги: FIFO по старейшим счетам
BillingHistory                 — проводки по балансу жителя
residents.balance              — кэш: переплата (+) / долг (−)
```

* **Charge**: общая сумма, период (метка + даты), способ распределения,
  статус `draft → issued → closed | cancelled`, номер `НАЧ-2026-0001`.
* **Invoice**: номер `СЧ-2026-…`, сумма, оплачено, остаток, срок,
  статус `pending → partial → paid`, `overdue`, `void` (сторно).
* **Payment**: сумма, дата, метод (наличные/перевод/карта/онлайн/бартер),
  внешний ID платёжной системы, ссылка на счёт(а).

Суммы хранятся **целыми копейками** (`Money::parse`), форматирование —
`Money::format` с неразрывными пробелами: `15 000 ₽`.

## Кто участвует в расчёте

В начислении участвуют жители, у которых одновременно:

* `is_active = 1` — чекбокс **«включен»** (отключённые не видны и не считаются),
* `is_billed = 1` — «участвует в оплатах»,
* не удалён (мягкое удаление).

Опционально (`billing.count_only_with_access`) — ещё и флаг «доступ».
Это правило едино для UI, CLI, API и перерасчётов.

## Начисление (мастер)

**Админка → Оплаты → Мастер** или
`bin/bas-ip billing:charge --total=15000 --period=2026-10 --notify --yes`.

1. **Общая сумма** → **предпросмотр**: участников 20, на каждого 750 ₽,
   сумма счетов = 15 000 ₽ **ровно** (расщепление `Money::split` распределяет
   остаток по копейке между первыми участниками — расхождение максимум
   1 копейка на счёт, сумма всегда сходится).
2. **Способ распределения** (`billing.allocation` или в мастере):
   * `equal` — поровну;
   * `by_area` — пропорционально площади квартир;
   * `by_persons` — по числу проживающих;
   * `by_share` — по доле собственности (`residents.share`).
3. **Период**: распознаются `10.2026`, `2026-10`, «Октябрь 2026», «октябрь»;
   произвольный текст сохраняется как есть. Даты периода → срок оплаты
   (`due_days` дней или точная `due_date`).
4. **Ссылка на оплату** (`pay_link`): подстановки `{id}`, `{apartment}`,
   `{sum}` (копейки/рубли по настройке), `{period}`, `{name}` — подставляются
   в каждый счёт индивидуально. Пример:
   `https://pay.example/charge?flat={apartment}&sum={sum}&month={period}`.
5. **Сообщение**: своё (с теми же плейсхолдерами) или автогенерируемое из
   шаблона `payment_new`.
6. **Выставить счета** → транзакция: charge + N invoices + (опционально)
   очередь уведомлений. Черновик (`draft`, `--no-issue`) — без счетов.

Результат в CLI:

```
ℹ 15 000 ₽ ÷ 20 активных = 750 ₽ на каждого
✔ Начисление CHG-2026-0001: выставлено 20 счетов по 750 ₽.
ℹ В рассылку поставлено 18 сообщений (пропущено 2).
```

(«пропущено» — отключённые жители или без каналов связи.)

## Приём оплат

* **UI**: карточка жителя → «Принять оплату»; Оплаты → Платежи → «Добавить».
* **CLI**: `billing:payment --resident=5 --amount=750 --method=cash`.
* **API**: `POST /api/v1/payments` (scope `billing.payment.record`) — для
  вебхука платёжного сервиса; `external_id` защищает от дублей.

Правила:

* оплата распределяется **FIFO**: сначала самый старый непогашенный счёт;
* частичные оплаты разрешены (`allow_partial`) — счёт становится `partial`;
* переплата остаётся на **балансе** жителя (`overpay_to_balance`) и видна
  в следующем начислении;
* отрицательные суммы запрещены (возврат = удаление платежа);
* удаление платежа **откатывает** счета обратно (pending/partial);
* просроченные счета помечает cron (`auto_mark_overdue`).

## Перерасчёт

Ситуация: сумма изменилась (15 000 → 20 000) или состав участников изменился
(кто-то въехал/выехал/включён-выключен). **Перерасчёт не трогает выставленные
счета** — он создаёт разницу:

* **Админка**: начисление → «Перерасчёт» → новая сумма/состав → предпросмотр;
* **CLI**: `billing:recalc --charge=CHG-2026-0001 --total=20000`.

Механика (проверена тестами):

* каждому участнику с недостающей суммой — **дополнительный счёт** на дельту
  (20 000 ÷ 20 = 1 000; было 750 → досчёт +250);
* **выключенным/выехавшим** — **сторно** (кредит-инвойс отрицательной суммой);
* **новые активные** добавляются в состав со своей долей;
* если ничего не изменилось — no-op, ничего не создаётся;
* все корректировки попадают в BillingHistory, уведомление — шаблон
  `charge_recalculated`.

Итог: сумма счетов начисления всегда соответствует последней утверждённой
сумме, история изменений сохраняется, дважды никто не платит.

## Должники и напоминания

* **Оплаты → Должники**: активные жители с непогашёнными/просроченными
  счетами; сортировки по сумме и давности; фильтр по периоду.
* **«Напомнить»** (UI) / `billing:remind` (CLI) — рассылка `payment_overdue`
  всем активным должникам одной очередью.
* Планировщик шлёт напоминания по расписанию `billing.reminder`
  (за 3 и 1 день до срока; через 1/7/30 дней после).
* Пени (`late_fee`) — опционально: процент или фикс после N дней.

## Балансы и целостность

* `residents.balance` — кэш; пересчёт — `billing:balances` (cron делает сам);
* сторнированные (`void`) счета игнорируются во всех итогах;
* каждое действие (начисление, оплата, удаление, перерасчёт) пишет
  BillingHistory и аудит.

## Экспорт и отчётность

* Начисление → «Экспорт» (CSV): все счета с оплатами — для бухгалтерии.
* `/admin/billing/payments` → экспорт платежей за период.
* API `GET /api/v1/stats` — сводки для внешних отчётов.

## Частые операции (шпаргалка)

| Задача | Действие |
|---|---|
| Начислить за месяц | Мастер: сумма → период → выставить |
| Принять наличные | Карточка жителя → Принять оплату |
| Житель заплатил онлайн | API `POST /api/v1/payments` или ręчной ввод с `external_id` |
| Изменилась смета | Начисление → Перерасчёт |
| Житель съехал | Выключить жителя → Перерасчёт (сторно) |
| Напомнить всем | Должники → Напомнить |
| Закрыть период | Начисление → Закрыть (read-only) |
| Сверить суммы | `billing:balances`, затем `billing:charges` |

---
Дальше: [09 · Уведомления](09-Notifications.md)
