Files
veilix.app/docs/04-billing.md
T
andreypr207andClaude Opus 5.5 515acfb69d Документация: этапы по фазам, решения D-026…D-033
- Новый порядок этапов (D-026): фаза 1 — концепт на dev без внешних
  сервисов (1b сайт, 1c вход, 1d ключи, 1e счёт и демо-оплата,
  1f демо-выдача, 1g продления, 1h рефералы); фаза 2 — Telegram;
  фаза 3 — ALTCHA, ЮKassa, почта, Twenty, Chatwoot, Umami, выдача
  через API и публичный запуск; фаза 4 — звёзды и эквайринги.
- Решения: демо-оплата по списку IP (D-027), выдача ключей через
  адаптер manual/demo/api (D-028), страницы сайта из блоков Payload
  (D-029), пересборка через revalidatePath (D-030), RU-адрес у mass
  (D-031), счётчик свободных серверов (D-032), переводы на 5 языков
  в каждом этапе (D-033).
- Тексты описывают работающий продукт; Вейликс — выделенный IP на
  общем сервере; Telegram поддержки — @veilix_support; сроки ответа
  не обещаем; VPN-серверы не ведут журналов.
- Ссылки на этапы во всех документах, Q-12 решён (ЮKassa для RUB),
  R-19 (библиотеки для headless Chromium) сделано.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 03:48:10 +02:00

200 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 дней» (предупреждающий цвет).