19 KiB
04. Биллинг
1. Принципы
- Всё через счёт. Любой успешный платёж зачисляется на баланс (движение
topup). Любой заказ оплачивается списанием с баланса (order_charge). Поэтому оплата картой, оплата с баланса, доплата разницы и пополнение заранее работают одним механизмом. - Баланс — это сумма движений по счёту. Поле
customers.balance— кэш, он обновляется в той же транзакции базы, что и запись движения. Раз в сутки задачаreconcileсверяет кэш с суммой. - Движения неизменяемы. Ошибка исправляется встречной записью.
- Одна точка проведения платежа —
core/billing/processPayment(paymentId). Её вызывают вебхук провайдера, ручной платёж из админки и тесты. Повторный вызов ничего не делает. - Суммы — в минимальных единицах валюты аккаунта.
2. Валюта аккаунта
- При регистрации валюта аккаунта = валюта, выбранная в переключателе (или валюта по умолчанию домена).
- Сменить валюту в кабинете можно, пока баланс нулевой и нет ключей и неоплаченных заказов. Дальше — через поддержку: администратор пересчитывает вручную корректировками.
- Цены в кабинете всегда в валюте аккаунта. Тариф без цены в этой валюте недоступен.
3. Покупка ключа (оформление заказа на my.*)
- Ссылка «Купить» с сайта ведёт на
my.<домен>/{язык}/order?tariff=…&location=…¤cy=…. - Шаг «Почта»: email и код (05-auth.md). Вошедший пользователь шаг пропускает.
- Сервер проверяет: тариф активен, есть цена в валюте аккаунта, локация доступна (dedicated:
availableилиon_demand). - Создаётся заказ
pending:new_key, сумма —firstPeriodAmount, действует 24 часа. Если у пользователя уже есть такой же неоплаченный заказ, используется он. - Шаг «Оплата»: сводка заказа, согласие с условиями (обязательная галочка со ссылками на юридические страницы) и способ оплаты.
a. Баланса хватает — кнопка «Оплатить с баланса», заказ оплачивается сразу.
b. Баланса не хватает — список платёжных систем: активные, в валюте аккаунта, с назначением
order, видимые пользователю. Создаётся платёжpendingна сумму «стоимость минус баланс» (показываем: «С баланса спишется X, к оплате Y»), дальше — страница или виджет провайдера (этап 4). - На этапе 1 видимых платёжных систем нет. Заказ создаётся, пользователь видит «Заказ создан, ожидает оплаты». Оплату проводит администратор ручным платежом, либо пользователь оплачивает с баланса, который пополнил администратор. Так проверяется весь сценарий без эквайринга.
- После оплаты: заказ
paid, ключ создаётся в статусеprovisioning, письмаorder_paidпользователю иadmin_provisioning_neededна почту поддержки.
4. Пополнение баланса
- Платёж с назначением
topup. Суммы — пресеты сайта для валюты (sites.topupPresets) или своя сумма в пределах лимитов платёжной системы. - После зачисления — письмо
topup_succeeded. - После любого пополнения система сама оплачивает с баланса неоплаченные заказы на продление этого пользователя, начиная со старого. Неоплаченные заказы на новые ключи сама не оплачивает.
- Криптовалюта (этап 4): счёт в монете, курс фиксируется на 15 минут, зачисление в валюте аккаунта после подтверждения сети.
5. Проведение платежа: processPayment
Выполняется, когда платёж переходит в succeeded, в одной транзакции:
- Заблокировать строку платежа. Если
processedAtзаполнен — выйти. - Заблокировать строку пользователя.
- Движение
+amount(topup). - Если назначение
orderи заказpending: баланса хватает — движение−order.amount(order_charge), заказpaid. Не хватает — заказ остаётсяpending, деньги остаются на балансе. - Заказ оплачен — исполнить:
new_keyсоздаёт ключprovisioningс копией цены продления, периода и лимита устройств;renewalпродлевает ключ (раздел 6). - Проставить
processedAt. - Проверить реферальное вознаграждение (раздел 11).
- Поставить в очередь письма, записать журнал действий.
Если заказ к моменту оплаты отменён или истёк, деньги остаются на балансе, пользователь получает письмо о зачислении.
Оплата заказа с баланса без внешнего платежа — те же шаги 2, 4, 5, 7, 8.
6. Продление
6.1 Автоматическое (задача renewals, каждые 15 минут)
Берём ключи в статусе active или grace с autoRenew = true, у которых expiresAt ≤ сейчас + renewalLeadHours и нет оплаченного заказа на продление с renewalFor = expiresAt. Для каждого, с блокировкой ключа:
- Найти или создать заказ
renewal:renewalFor = expiresAt, сумма —renewalAmountключа. - Баланса хватает — списание, заказ
paid,expiresAt = renewalFor + period, статусactive, письмоrenewal_succeeded. - Не хватает, но есть сохранённая карта с автосписанием (Stripe, этап 4) — платёж на разницу без участия пользователя. Неудача — повтор: за 24 часа до окончания, в момент окончания, через 24 и 48 часов (внутри льготного периода). После первой неудачи — письмо
renewal_failed. - Нет ни денег, ни карты — заказ ждёт оплаты. Его оплатит пополнение (раздел 4.3) или кнопка «Продлить сейчас».
6.2 «Продлить сейчас»
Доступна для ключей active и grace. Создаёт (или берёт существующий) заказ renewal на renewalFor = expiresAt и оплачивает его с баланса или через платёжную систему. Ключ продлевается на период от текущего expiresAt. Можно продлить заранее несколько раз подряд.
6.3 Автопродление и карта
- Галочка автопродления — у каждого ключа.
- «Отвязать карту» — в аккаунте, раздел «Счёт и платежи». После отвязки автопродление списывает только баланс.
7. Льготный период и окончание (задача expiry, каждые 15 минут)
active, срок истёк, продление не оплачено →grace,graceUntil = expiresAt + graceDays, письмоgrace_started. Ключ работает.- Продление оплачено в льготный период →
active, новый срок считается от старогоexpiresAt: бесплатных дней нет. grace,graceUntilнаступил →expired, неоплаченный заказ на продление →expired, письмоkey_expired. На этапе 1 ключ попадает в список «К отключению» в админке; отключение автоматическое — этап 5.- Правило одинаковое для ключей с автопродлением и без. Без автопродления пользователь сам решает, продлевать ли в льготный период.
- Истёкший ключ не продлевается: покупается новый. Администратор может вернуть ключ вручную.
8. Выдача ключа
- Админка, ключ в статусе
provisioning: администратор добавляет подключения (WG/AWG по устройствам, ссылку Happ, RU-адрес), IP-адреса и нажимает «Выдать ключ». - Нужно хотя бы одно активное подключение. Затем:
active,activatedAt = сейчас,expiresAt = сейчас + период, письмоkey_ready. - Список «Очередь выдачи» в админке: ключи
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. Возвраты
- Политика: если сервер не заработал по нашей вине — полностью в течение первых 7 дней; если пользователь передумал — остаток неиспользованного периода. Сумму считает администратор, система подсказывает остаток пропорционально дням.
- Этап 1 — только возврат на баланс.
- Этап 4 — возврат на исходный способ оплаты, если провайдер умеет. Запись
refund(+) на баланс по заказу, затемpayout(−) — деньги уходят на карту. Итог: баланс не меняется, деньги у пользователя на карте.
11. Реферальное вознаграждение
- Привязка: при регистрации, если есть cookie
vx_ref(ставитmy.<домен>/r/<код>, живёт 60 дней) или параметр/start <код>в боте. Код того же сайта, не свой. Создаётся приглашениеpending. - Условие: оплачен первый заказ
new_keyприглашённого. Пополнение баланса условием не считается. - Проверка: совпадает хэш IP регистрации приглашённого и пригласившего →
review, решает администратор. Иначе — начисление. - Начисление: движение
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 Ключ
stateDiagram-v2
[*] --> provisioning: заказ оплачен
provisioning --> active: выдан
active --> active: продление оплачено
active --> grace: срок истёк, оплаты нет
grace --> active: продление оплачено
grace --> expired: льготный период закончился
provisioning --> cancelled: возврат
active --> cancelled: возврат с отменой
active --> suspended: блокировка
suspended --> active: разблокировка
13.2 Заказ
stateDiagram-v2
[*] --> pending
pending --> paid
pending --> cancelled: отменён
pending --> expired: истёк срок заказа или ключ истёк
paid --> partially_refunded
paid --> refunded
partially_refunded --> refunded
13.3 Платёж
stateDiagram-v2
[*] --> pending
pending --> succeeded
pending --> failed
pending --> cancelled
succeeded --> partially_refunded
succeeded --> refunded
14. Конкурентность и идемпотентность
- Все денежные операции — в транзакции базы. Баланс меняется только под блокировкой строки пользователя (
SELECT … FOR UPDATE). - Уникальные ограничения защищают от двойного проведения: движения (см. 03-data-model.md, 6.3), платежи (
adapter+providerPaymentId), заказы (key+renewalFor). - Вебхуки: проверка подписи, сохранение id события, идемпотентная обработка, быстрый ответ 200.
- Задачи берут ключи с
FOR UPDATE SKIP LOCKED, чтобы параллельные запуски не продлили ключ дважды.
15. Как показываем в кабинете
- История платежей — одна строка на событие для человека. Платёж картой за заказ (зачисление и списание) показывается одной строкой: «Первый месяц · Нидерланды · Амстердам · Карта ···· 4242 · −4 900 ₽». Списание с баланса — «С баланса». Пополнение, реферальный бонус, возврат, корректировка — отдельными строками.
- «Следующее списание» — ближайшее продление среди ключей с автопродлением: сумма, дата, ключ; ниже — следующие.
- Бейдж «Истекает» — до окончания не больше
expiringBadgeDaysи (автопродление выключено или не хватит ни баланса, ни карты). - На странице ключа: «Продлится автоматически через N дней», если денег или карты хватает, иначе «Осталось N дней» (предупреждающий цвет).