Files
veilix.app/docs/04-billing.md
T

199 lines
21 KiB
Markdown
Raw Normal View History

# 04. Биллинг
## 1. Принципы
1. Всё через счёт. Любой успешный платёж зачисляется на баланс (движение `topup`). Любой заказ оплачивается списанием с баланса (`order_charge`). Поэтому оплата картой, оплата с баланса, доплата разницы и пополнение заранее работают одним механизмом.
2. Баланс — это сумма движений по счёту. Поле `customers.balance` — кэш, он обновляется в той же транзакции базы, что и запись движения. Раз в сутки задача `reconcile` сверяет кэш с суммой.
3. Движения неизменяемы. Ошибка исправляется встречной записью.
4. Одна точка проведения платежа — `core/billing/processPayment(paymentId)`. Её вызывают вебхук провайдера, ручной платёж из админки и тесты. Повторный вызов ничего не делает.
5. Суммы — в минимальных единицах валюты аккаунта.
## 2. Валюта аккаунта
1. При регистрации валюта аккаунта = валюта, выбранная в переключателе (или валюта по умолчанию домена).
2. Сменить валюту в кабинете можно, пока баланс нулевой и нет ключей и неоплаченных заказов. Дальше — через поддержку: администратор пересчитывает вручную корректировками.
3. Цены в кабинете всегда в валюте аккаунта. Тариф без цены в этой валюте недоступен.
## 3. Покупка ключа (оформление заказа на my.*)
1. Ссылка «Купить» с сайта ведёт на `my.<домен>/{язык}/order?tariff=…&location=…&currency=…`.
2. Шаг «Почта»: email и код ([05-auth.md](05-auth.md)). Вошедший пользователь шаг пропускает.
3. Сервер проверяет: тариф активен, есть цена в валюте аккаунта, локация доступна (dedicated: `available` или `on_demand`).
4. Создаётся заказ `pending`: `new_key`, сумма — `firstPeriodAmount`, действует 24 часа. Если у пользователя уже есть такой же неоплаченный заказ, используется он.
5. Шаг «Оплата»: сводка заказа, согласие с условиями (обязательная галочка со ссылками на юридические страницы) и способ оплаты.
a. Баланса хватает — кнопка «Оплатить с баланса», заказ оплачивается сразу.
b. Баланса не хватает — список платёжных систем: активные, в валюте аккаунта, с назначением `order`, видимые пользователю. Создаётся платёж `pending` на сумму «стоимость минус баланс» (показываем: «С баланса спишется X, к оплате Y»), дальше — страница или виджет провайдера: демо-оплата (этап 1e), Stripe (4b).
6. Если видимых платёжных систем нет, заказ создаётся, пользователь видит «Заказ создан, ожидает оплаты». Оплату проводит администратор ручным платежом, либо пользователь оплачивает с баланса, который пополнил администратор. В фазе 1 «Демо» видна только с разрешённых IP ([D-027](decisions/D-027-demo-payments.md)).
7. После оплаты: заказ `paid`, ключ создаётся в статусе `provisioning`, письма `order_paid` пользователю и `admin_provisioning_needed` на почту поддержки. Дальше ключ выдаёт адаптер выдачи ([D-028](decisions/D-028-provisioning-adapter.md)).
## 4. Пополнение баланса
1. Платёж с назначением `topup`. Суммы — пресеты сайта для валюты (`sites.topupPresets`) или своя сумма в пределах лимитов платёжной системы. Пресеты: dedicated — 2 500, 5 000, 10 000 ₽ и $25, $50, $100; mass — 700, 2 100, 6 990 ₽ и $7, $21, $70 (Q-02).
2. После зачисления — письмо `topup_succeeded`.
3. После любого пополнения система сама оплачивает с баланса неоплаченные заказы на продление этого пользователя, начиная со старого. Неоплаченные заказы на новые ключи сама не оплачивает.
4. Криптовалюта (этап 4b): счёт в монете, курс фиксируется на 15 минут, зачисление в валюте аккаунта после подтверждения сети.
## 5. Проведение платежа: `processPayment`
Выполняется, когда платёж переходит в `succeeded`, в одной транзакции:
1. Заблокировать строку платежа. Если `processedAt` заполнен — выйти.
2. Заблокировать строку пользователя.
3. Движение `+amount` (`topup`).
4. Если назначение `order` и заказ `pending`: баланса хватает — движение `−order.amount` (`order_charge`), заказ `paid`. Не хватает — заказ остаётся `pending`, деньги остаются на балансе.
5. Заказ оплачен — исполнить: `new_key` создаёт ключ `provisioning` с копией цены продления, периода и лимита устройств; `renewal` продлевает ключ (раздел 6).
6. Проставить `processedAt`.
7. Проверить реферальное вознаграждение (раздел 11).
8. Поставить в очередь письма, записать журнал действий.
Если заказ к моменту оплаты отменён или истёк, деньги остаются на балансе, пользователь получает письмо о зачислении.
Оплата заказа с баланса без внешнего платежа — те же шаги 2, 4, 5, 7, 8.
## 6. Продление
### 6.1 Автоматическое (задача `renewals`, каждые 15 минут)
Берём ключи в статусе `active` или `grace` с `autoRenew = true`, у которых `expiresAt ≤ сейчас + renewalLeadHours` и нет оплаченного заказа на продление с `renewalFor = expiresAt`. Для каждого, с блокировкой ключа:
1. Найти или создать заказ `renewal`: `renewalFor = expiresAt`, сумма — `renewalAmount` ключа.
2. Баланса хватает — списание, заказ `paid`, `expiresAt = renewalFor + period`, статус `active`, письмо `renewal_succeeded`.
3. Не хватает, но есть сохранённая карта с автосписанием (демо-карта — этап 1g, Stripe — 4b) — платёж на разницу без участия пользователя. Неудача — повтор: за 24 часа до окончания, в момент окончания, через 24 и 48 часов (внутри льготного периода). После первой неудачи — письмо `renewal_failed`.
4. Нет ни денег, ни карты — заказ ждёт оплаты. Его оплатит пополнение (раздел 4.3) или кнопка «Продлить сейчас».
### 6.2 «Продлить сейчас»
Доступна для ключей `active` и `grace`. Создаёт (или берёт существующий) заказ `renewal` на `renewalFor = expiresAt` и оплачивает его с баланса или через платёжную систему. Ключ продлевается на период от текущего `expiresAt`. Можно продлить заранее несколько раз подряд.
### 6.3 Автопродление и карта
1. Галочка автопродления — у каждого ключа.
2. «Отвязать карту» — в аккаунте, раздел «Счёт и платежи». После отвязки автопродление списывает только баланс.
### 6.4 Продление без оплаты
Реферальный бонус (раздел 11) и «Продлить вручную» в админке (раздел 9) добавляют к сроку ключа N дней без денег. Каждое такое продление — запись `key-extensions` с причиной.
1. Ключ `active` или `grace`: `expiresAt` сдвигается на N дней. Ключ `grace`, у которого новый срок ещё не наступил, снова `active`. Неоплаченный заказ на продление на старую дату отменяется: задача `renewals` создаст новый к новой дате.
2. Ключ `provisioning`: дни ждут выдачи и добавляются к сроку при ней (раздел 8).
3. Истёкший, отменённый и заблокированный ключи так не продлеваются.
## 7. Льготный период и окончание (задача `expiry`, каждые 15 минут)
1. `active`, срок истёк, продление не оплачено → `grace`, `graceUntil = expiresAt + graceDays`, письмо `grace_started`. Ключ работает.
2. Продление оплачено в льготный период → `active`, новый срок считается от старого `expiresAt`: бесплатных дней нет.
3. `grace`, `graceUntil` наступил → `expired`, неоплаченный заказ на продление → `expired`, письмо `key_expired`. Подключения отключает адаптер выдачи: у `manual` ключ попадает в список «К отключению» в админке, у `demo` и `api` — автоматически ([D-028](decisions/D-028-provisioning-adapter.md)).
4. Правило одинаковое для ключей с автопродлением и без. Без автопродления пользователь сам решает, продлевать ли в льготный период.
5. Истёкший ключ не продлевается: покупается новый. Администратор может вернуть ключ вручную.
## 8. Выдача ключа
1. Режим `manual`: в админке у ключа в статусе `provisioning` администратор добавляет подключения (WG/AWG по устройствам, ссылку Happ, RU-адрес), IP-адреса и нажимает «Выдать ключ». В режимах `demo` и `api` это делает адаптер выдачи ([D-028](decisions/D-028-provisioning-adapter.md)).
2. Нужно хотя бы одно активное подключение. Затем: `active`, `activatedAt = сейчас`, `expiresAt = сейчас + период` плюс дни продлений без оплаты, которые ждали выдачи (раздел 6.4), письмо `key_ready`.
3. Список «Очередь выдачи» в админке: ключи `provisioning` по времени создания; локации `on_demand` помечены сроком «24 часа».
## 9. Ручные операции в админке
| Операция | Что происходит |
|---|---|
| Ручной платёж | Платёж через платёжную систему `manual`: пользователь, назначение, заказ (необязательно), сумма в валюте аккаунта, статус, комментарий. Когда статус становится `succeeded`, срабатывает `processPayment` — как у настоящего платежа. Можно завести `pending` (ждём перевод) и позже перевести в `succeeded`. Галочки «провести как реальную» нет: проводится всегда |
| Корректировка баланса | Движение `adjustment` ±сумма, комментарий обязателен. Цепочка действий не запускается |
| Возврат на баланс | На заказе: сумма (не больше оплаченной минус возвращённое), галочка «Отменить ключ». Движение `refund`, заказ `refunded` или `partially_refunded`, ключ `cancelled`, если отмечено. Письмо `refund_issued` |
| Выдать ключ | `provisioning` → `active` (раздел 8) |
| Продлить вручную | Продлить ключ на N дней без оплаты (компенсация, раздел 6.4), комментарий обязателен |
| Заблокировать | Пользователь `blocked`: войти нельзя. Ключ `suspended` |
Все операции пишутся в журнал действий.
## 10. Возвраты
1. Политика: если сервер не заработал по нашей вине — полностью в течение первых 7 дней; если пользователь передумал — остаток неиспользованного периода. Сумму считает администратор, система подсказывает остаток пропорционально дням.
2. До этапа 4b — только возврат на баланс.
3. Этап 4b — возврат на исходный способ оплаты, если провайдер умеет. Запись `refund` (+) на баланс по заказу, затем `payout` (−) — деньги уходят на карту. Итог: баланс не меняется, деньги у пользователя на карте.
## 11. Реферальное вознаграждение
1. Привязка: при регистрации, если есть cookie `vx_ref` (ставит `my.<домен>/r/<код>`, живёт 60 дней) или параметр `/start <код>` в боте. Код того же сайта, не свой. Создаётся приглашение `pending`.
2. Условие: оплачен первый заказ `new_key` приглашённого. Пополнение баланса условием не считается.
3. Проверка: совпадает хэш IP регистрации приглашённого и пригласившего → `review`, решает администратор. Иначе — начисление.
4. Начисление — дни, а не деньги ([D-025](decisions/D-025-referral-days.md)): обоим по `sites.referralBonusDays` (по умолчанию 30) продлением без оплаты (раздел 6.4). Приглашение `rewarded`, письма `referral_reward` обоим.
a. Приглашённому — на ключ из заказа, который выполнил условие.
b. Пригласившему — на последний купленный ключ в статусе `provisioning`, `active` или `grace`. Такого нет — дни ждут и добавляются к следующему ключу пригласившего, когда он будет создан.
5. Возврат по заказу приглашённого бонус пригласившего не отменяет; злоупотребления разбирает администратор.
## 12. Уведомления
| Когда | Шаблон | Условие |
|---|---|---|
| Заказ оплачен | `order_paid` | всегда |
| Ключ ждёт подготовки (24 часа) | `key_provisioning` | локация `on_demand` |
| Ключ выдан | `key_ready` | |
| За `reminderDays` до окончания | `renewal_reminder` | автопродление включено, но баланса и карты не хватит |
| За `reminderDays` до окончания | `expiry_reminder` | автопродление выключено |
| Продление прошло | `renewal_succeeded` | |
| Карта не прошла | `renewal_failed` | |
| Начался льготный период | `grace_started` | |
| Ключ истёк | `key_expired` | |
| Баланс пополнен | `topup_succeeded` | |
| Реферальный бонус | `referral_reward` | |
| Возврат | `refund_issued` | |
| Администраторам | `admin_new_order`, `admin_provisioning_needed`, `admin_new_lead` | на почту поддержки сайта |
С этапа 2b те же уведомления дублируются в основного бота, если Telegram привязан.
## 13. Статусы
### 13.1 Ключ
```mermaid
stateDiagram-v2
[*] --> provisioning: заказ оплачен
provisioning --> active: выдан
active --> active: продление оплачено
active --> grace: срок истёк, оплаты нет
grace --> active: продление оплачено
grace --> expired: льготный период закончился
provisioning --> cancelled: возврат
active --> cancelled: возврат с отменой
active --> suspended: блокировка
suspended --> active: разблокировка
```
### 13.2 Заказ
```mermaid
stateDiagram-v2
[*] --> pending
pending --> paid
pending --> cancelled: отменён
pending --> expired: истёк срок заказа или ключ истёк
paid --> partially_refunded
paid --> refunded
partially_refunded --> refunded
```
### 13.3 Платёж
```mermaid
stateDiagram-v2
[*] --> pending
pending --> succeeded
pending --> failed
pending --> cancelled
succeeded --> partially_refunded
succeeded --> refunded
```
## 14. Конкурентность и идемпотентность
1. Все денежные операции — в транзакции базы. Баланс меняется только под блокировкой строки пользователя (`SELECT … FOR UPDATE`).
2. Уникальные ограничения защищают от двойного проведения: движения (см. [03-data-model.md](03-data-model.md), 6.3), платежи (`adapter` + `providerPaymentId`), заказы (`key` + `renewalFor`).
3. Вебхуки: проверка подписи, сохранение id события, идемпотентная обработка, быстрый ответ 200.
4. Задачи берут ключи с `FOR UPDATE SKIP LOCKED`, чтобы параллельные запуски не продлили ключ дважды.
## 15. Как показываем в кабинете
1. История платежей — одна строка на событие для человека. Платёж картой за заказ (зачисление и списание) показывается одной строкой: «Первый месяц · Нидерланды · Амстердам · Карта ···· 4242 · −4 900 ₽». Списание с баланса — «С баланса». Пополнение, реферальный бонус, возврат, корректировка — отдельными строками.
2. «Следующее списание» — ближайшее продление среди ключей с автопродлением: сумма, дата, ключ; ниже — следующие.
3. Бейдж «Истекает» — до окончания не больше `expiringBadgeDays` и (автопродление выключено или не хватит ни баланса, ни карты).
4. На странице ключа: «Продлится автоматически через N дней», если денег или карты хватает, иначе «Осталось N дней» (предупреждающий цвет).