Files
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

21 KiB
Raw Permalink Blame History

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). Вошедший пользователь шаг пропускает.
  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).
  7. После оплаты: заказ paid, ключ создаётся в статусе provisioning, письма order_paid пользователю и admin_provisioning_needed на почту поддержки. Дальше ключ выдаёт адаптер выдачи (D-028).

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).
  4. Правило одинаковое для ключей с автопродлением и без. Без автопродления пользователь сам решает, продлевать ли в льготный период.
  5. Истёкший ключ не продлевается: покупается новый. Администратор может вернуть ключ вручную.

8. Выдача ключа

  1. Режим manual: в админке у ключа в статусе provisioning администратор добавляет подключения (WG/AWG по устройствам, ссылку Happ, RU-адрес), IP-адреса и нажимает «Выдать ключ». В режимах demo и api это делает адаптер выдачи (D-028).
  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): обоим по 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 Ключ

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. Конкурентность и идемпотентность

  1. Все денежные операции — в транзакции базы. Баланс меняется только под блокировкой строки пользователя (SELECT … FOR UPDATE).
  2. Уникальные ограничения защищают от двойного проведения: движения (см. 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 дней» (предупреждающий цвет).