# 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 дней» (предупреждающий цвет).