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»), дальше — страница или виджет провайдера: демо-оплата (этап 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)).
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).
3. После любого пополнения система сама оплачивает с баланса неоплаченные заказы на продление этого пользователя, начиная со старого. Неоплаченные заказы на новые ключи сама не оплачивает.
Выполняется, когда платёж переходит в `succeeded`, в одной транзакции:
1. Заблокировать строку платежа. Если `processedAt` заполнен — выйти.
2. Заблокировать строку пользователя.
3. Движение `+amount` (`topup`).
4. Если назначение `order` и заказ `pending`: баланса хватает — движение `−order.amount` (`order_charge`), заказ `paid`. Не хватает — заказ остаётся `pending`, деньги остаются на балансе.
5. Заказ оплачен — исполнить: `new_key` создаёт ключ `provisioning` с копией цены продления, периода и лимита устройств; `renewal` продлевает ключ (раздел 6).
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. «Отвязать карту» — в аккаунте, раздел «Счёт и платежи». После отвязки автопродление списывает только баланс.
Реферальный бонус (раздел 11) и «Продлить вручную» в админке (раздел 9) добавляют к сроку ключа N дней без денег. Каждое такое продление — запись `key-extensions`с причиной.
1. Ключ `active` или `grace`: `expiresAt` сдвигается на N дней. Ключ `grace`, у которого новый срок ещё не наступил, снова `active`. Неоплаченный заказ на продление на старую дату отменяется: задача `renewals` создаст новый к новой дате.
2. Ключ `provisioning`: дни ждут выдачи и добавляются к сроку при ней (раздел 8).
3. Истёкший, отменённый и заблокированный ключи так не продлеваются.
3.`grace`, `graceUntil` наступил → `expired`, неоплаченный заказ на продление → `expired`, письмо `key_expired`. Подключения отключает адаптер выдачи: у `manual` ключ попадает в список «К отключению» в админке, у `demo` и `api` — автоматически ([D-028](decisions/D-028-provisioning-adapter.md)).
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` |
1. Политика: если сервер не заработал по нашей вине — полностью в течение первых 7 дней; если пользователь передумал — остаток неиспользованного периода. Сумму считает администратор, система подсказывает остаток пропорционально дням.
3. Этап 4b — возврат на исходный способ оплаты, если провайдер умеет. Запись `refund` (+) на баланс по заказу, затем `payout` (−) — деньги уходят на карту. Итог: баланс не меняется, деньги у пользователя на карте.
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. Возврат по заказу приглашённого бонус пригласившего не отменяет; злоупотребления разбирает администратор.
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 дней» (предупреждающий цвет).