Этап 0: документация и макеты
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
3400f58a5a
75 files changed
+14283
No files matched your search
@@ -0,0 +1,188 @@
|
||||
# 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=…¤cy=…`.
|
||||
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»), дальше — страница или виджет провайдера (этап 4).
|
||||
6. На этапе 1 видимых платёжных систем нет. Заказ создаётся, пользователь видит «Заказ создан, ожидает оплаты». Оплату проводит администратор ручным платежом, либо пользователь оплачивает с баланса, который пополнил администратор. Так проверяется весь сценарий без эквайринга.
|
||||
7. После оплаты: заказ `paid`, ключ создаётся в статусе `provisioning`, письма `order_paid` пользователю и `admin_provisioning_needed` на почту поддержки.
|
||||
|
||||
## 4. Пополнение баланса
|
||||
|
||||
1. Платёж с назначением `topup`. Суммы — пресеты сайта для валюты (`sites.topupPresets`) или своя сумма в пределах лимитов платёжной системы.
|
||||
2. После зачисления — письмо `topup_succeeded`.
|
||||
3. После любого пополнения система сама оплачивает с баланса неоплаченные заказы на продление этого пользователя, начиная со старого. Неоплаченные заказы на новые ключи сама не оплачивает.
|
||||
4. Криптовалюта (этап 4): счёт в монете, курс фиксируется на 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. Не хватает, но есть сохранённая карта с автосписанием (Stripe, этап 4) — платёж на разницу без участия пользователя. Неудача — повтор: за 24 часа до окончания, в момент окончания, через 24 и 48 часов (внутри льготного периода). После первой неудачи — письмо `renewal_failed`.
|
||||
4. Нет ни денег, ни карты — заказ ждёт оплаты. Его оплатит пополнение (раздел 4.3) или кнопка «Продлить сейчас».
|
||||
|
||||
### 6.2 «Продлить сейчас»
|
||||
|
||||
Доступна для ключей `active` и `grace`. Создаёт (или берёт существующий) заказ `renewal` на `renewalFor = expiresAt` и оплачивает его с баланса или через платёжную систему. Ключ продлевается на период от текущего `expiresAt`. Можно продлить заранее несколько раз подряд.
|
||||
|
||||
### 6.3 Автопродление и карта
|
||||
|
||||
1. Галочка автопродления — у каждого ключа.
|
||||
2. «Отвязать карту» — в аккаунте, раздел «Счёт и платежи». После отвязки автопродление списывает только баланс.
|
||||
|
||||
## 7. Льготный период и окончание (задача `expiry`, каждые 15 минут)
|
||||
|
||||
1. `active`, срок истёк, продление не оплачено → `grace`, `graceUntil = expiresAt + graceDays`, письмо `grace_started`. Ключ работает.
|
||||
2. Продление оплачено в льготный период → `active`, новый срок считается от старого `expiresAt`: бесплатных дней нет.
|
||||
3. `grace`, `graceUntil` наступил → `expired`, неоплаченный заказ на продление → `expired`, письмо `key_expired`. На этапе 1 ключ попадает в список «К отключению» в админке; отключение автоматическое — этап 5.
|
||||
4. Правило одинаковое для ключей с автопродлением и без. Без автопродления пользователь сам решает, продлевать ли в льготный период.
|
||||
5. Истёкший ключ не продлевается: покупается новый. Администратор может вернуть ключ вручную.
|
||||
|
||||
## 8. Выдача ключа
|
||||
|
||||
1. Админка, ключ в статусе `provisioning`: администратор добавляет подключения (WG/AWG по устройствам, ссылку Happ, RU-адрес), IP-адреса и нажимает «Выдать ключ».
|
||||
2. Нужно хотя бы одно активное подключение. Затем: `active`, `activatedAt = сейчас`, `expiresAt = сейчас + период`, письмо `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 дней без оплаты (компенсация) |
|
||||
| Заблокировать | Пользователь `blocked`: войти нельзя. Ключ `suspended` |
|
||||
|
||||
Все операции пишутся в журнал действий.
|
||||
|
||||
## 10. Возвраты
|
||||
|
||||
1. Политика: если сервер не заработал по нашей вине — полностью в течение первых 7 дней; если пользователь передумал — остаток неиспользованного периода. Сумму считает администратор, система подсказывает остаток пропорционально дням.
|
||||
2. Этап 1 — только возврат на баланс.
|
||||
3. Этап 4 — возврат на исходный способ оплаты, если провайдер умеет. Запись `refund` (+) на баланс по заказу, затем `payout` (−) — деньги уходят на карту. Итог: баланс не меняется, деньги у пользователя на карте.
|
||||
|
||||
## 11. Реферальное вознаграждение
|
||||
|
||||
1. Привязка: при регистрации, если есть cookie `vx_ref` (ставит `my.<домен>/r/<код>`, живёт 60 дней) или параметр `/start <код>` в боте. Код того же сайта, не свой. Создаётся приглашение `pending`.
|
||||
2. Условие: оплачен первый заказ `new_key` приглашённого. Пополнение баланса условием не считается.
|
||||
3. Проверка: совпадает хэш IP регистрации приглашённого и пригласившего → `review`, решает администратор. Иначе — начисление.
|
||||
4. Начисление: движение `referral_bonus` обоим, в валюте каждого, сумма из `sites.referralBonus`. Нет суммы для валюты — бонус не начисляется, в журнале предупреждение. Приглашение `rewarded`, письма `referral_reward`.
|
||||
|
||||
## 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` | на почту поддержки сайта |
|
||||
|
||||
С этапа 3 те же уведомления дублируются в основного бота, если 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 дней» (предупреждающий цвет).
|
||||
Reference in new issue
Block a user