Refactor billing configuration management to centralize expiration handling
- Updated `BillingConfigResumer` to manage billing expiration entirely on the application side, using `enable` as the sole control mechanism for client access. - Changed all relevant methods to pass `DateTimeOffset.UnixEpoch` for `expiresAt`, ensuring the panel does not enforce expiration independently of our application logic. - Modified `IXuiPanelGateway` interface to reflect the new expiration handling approach, clarifying the role of `expiresAt` in client management. - Adjusted command handlers for creating and rotating VPN configurations to set `expiresAt` to `null`, preventing unintended expiration enforcement by the panel. - Enhanced documentation to explain the new billing expiration management strategy and its implications for client configurations.
This commit is contained in:
+29
-29
@@ -544,32 +544,31 @@ Singleton (как `PricingSettings`) — реквизиты для оплаты
|
||||
|
||||
`Application/Billing/BillingConfigResumer.cs` — общая точка для всей синхронизации панельного клиента
|
||||
с оплатой; используется и `BillingService` (Infrastructure, за счёт направления зависимостей
|
||||
`Infrastructure → Application`), и Application-хендлерами напрямую. Три операции, все — через
|
||||
`IXuiPanelGateway.UpdateClientAsync(..., enable: ..., expiresAt: ..., ...)`, и **всегда передают оба
|
||||
параметра явно** (не полагаясь на один `enable` или один `expiryTime`) — по факту эксплуатации ни один
|
||||
из двух по отдельности не даёт достаточной гарантии (переключение `enable` не всегда обрывает уже
|
||||
установленные соединения на стороне Xray/панели, поэтому дублируем просроченным/будущим `expiryTime`):
|
||||
`Infrastructure → Application`), и Application-хендлерами напрямую. Истечение по биллингу управляется
|
||||
**полностью на нашей стороне** — единственный рычаг на панели это `enable`; `expiresAt` во всех трёх
|
||||
операциях всегда передаётся как `DateTimeOffset.UnixEpoch` (панель/Xray трактует `expiryTime == 0` как
|
||||
«без ограничения по сроку»), а не `null` — `UpdateClientAsync` с `expiresAt: null` означает «не
|
||||
трогать», чего недостаточно, чтобы гарантированно снять унаследованный от старых версий реальный
|
||||
`expiryTime`, если он когда-то был запушен. Три операции, все — через
|
||||
`IXuiPanelGateway.UpdateClientAsync(..., enable: ..., expiresAt: ..., ...)`:
|
||||
|
||||
- **`SuspendConfigsAsync`** — приостановка: `enable: false`, `expiresAt = UtcNow.AddDays(-1)`
|
||||
(гарантированно просроченная дата). Проходит и по `Active`, и по уже `Expired` конфигам (последние
|
||||
могли быть временно защищены `ProtectPendingConfigsAsync` — см. ниже, — защиту с них тоже нужно снять
|
||||
при отклонении заявки, не только локальный статус). Вызывается из `BillingService` (часовой тик) и из
|
||||
`RejectPaymentRequestCommandHandler` (немедленно при отклонении — см. ниже).
|
||||
- **`ResumeConfigsAsync`** — подтверждённая оплата/продление/гифт: `enable: true`, `expiresAt =
|
||||
newPaidUntil` (реальный новый срок, не «снять ограничение» на бесконечность — так панель/Xray сама
|
||||
несёт актуальный срок и продолжит блокировать по его истечении, даже если `BillingService` вдруг
|
||||
пропустит тик). Пушится на панель для **любого** статуса конфига, не только `Expired` — пользователь
|
||||
мог продлить/доплатить заранее, пока конфиг ещё `Active`, и панель должна узнать новый срок сразу, а
|
||||
не только когда конфиг реально просрочится и `BillingService` его тронет.
|
||||
- **`ProtectPendingConfigsAsync`** — заявка на оплату ждёт решения админа: `enable: true`, `expiresAt =
|
||||
UtcNow + BillingSettings.GraceDays` — временный грейс, пока админ проверяет. **Не трогает** `Status`/
|
||||
`ExpiresAt` в БД — это провизорная мера на панели до `Confirm` (`ResumeConfigsAsync` проставит
|
||||
настоящий срок) или `Reject` (`SuspendConfigsAsync` вернёт как было). Нужна отдельно от простого
|
||||
«пропустить приостановку», потому что Xray сам проверяет `expiryTime` клиента независимо от нашего
|
||||
локального статуса — если реальный `BillingPaidUntil` уже в прошлом, панель заблокирует клиента сама,
|
||||
даже если наше приложение ничего не приостанавливало. Вызывается сразу при `MarkPaymentSentCommandHandler`
|
||||
(не ждём часовой тик — пользователь отметил оплату, конфиги должны остаться рабочими немедленно) и
|
||||
повторно на каждом тике `BillingService`, пока заявка висит (идемпотентно продлевает грейс).
|
||||
- **`SuspendConfigsAsync`** — приостановка: `enable: false`. Проходит и по `Active`, и по уже
|
||||
`Expired` конфигам (последние могли быть временно защищены `ProtectPendingConfigsAsync` — см.
|
||||
ниже, — защиту с них тоже нужно снять при отклонении заявки, не только локальный статус). Вызывается
|
||||
из `BillingService` (часовой тик) и из `RejectPaymentRequestCommandHandler` (немедленно при
|
||||
отклонении — см. ниже).
|
||||
- **`ResumeConfigsAsync`** — подтверждённая оплата/продление/гифт: `enable: true`. Реальный новый срок
|
||||
фиксируется только локально (`config.SetBillingExpiry(newPaidUntil)`, денормализация
|
||||
`AppUser.BillingPaidUntil` для отображения и `Subscription-Userinfo`) — панель им не управляет.
|
||||
Пушится на панель для **любого** статуса конфига, не только `Expired` — пользователь мог
|
||||
продлить/доплатить заранее, пока конфиг ещё `Active`, и панель должна снять возможную блокировку
|
||||
сразу, а не только когда `BillingService` в следующий раз тронет конфиг.
|
||||
- **`ProtectPendingConfigsAsync`** — заявка на оплату ждёт решения админа: `enable: true`. **Не
|
||||
трогает** `Status`/`ExpiresAt` в БД — это провизорная мера на панели до `Confirm`
|
||||
(`ResumeConfigsAsync`) или `Reject` (`SuspendConfigsAsync` вернёт как было). Вызывается сразу при
|
||||
`MarkPaymentSentCommandHandler` (не ждём часовой тик — пользователь отметил оплату, конфиги должны
|
||||
остаться рабочими немедленно) и повторно на каждом тике `BillingService`, пока заявка висит
|
||||
(идемпотентно).
|
||||
|
||||
Симметрично: если админ **отклоняет** заявку (`RejectPaymentRequestCommandHandler`) и период всё ещё
|
||||
просрочен, а других Subscription-заявок на проверке нет — `SuspendConfigsAsync` вызывается немедленно,
|
||||
@@ -596,10 +595,11 @@ Application-хендлере поверх результата `IIdentityService
|
||||
граница Identity/биллинг не нарушается).
|
||||
|
||||
Прочие точки, не входящие в `BillingConfigResumer`:
|
||||
- **Создание** (`CreateVpnConfigCommandHandler`) пушит `expiresAt = profile.BillingPaidUntil` при
|
||||
`BillingEnabled` через `AddClientAsync` — свежий конфиг сразу несёт правильный срок.
|
||||
- **Ротация** (`RotateVpnConfigCommandHandler`) переносит текущий `config.ExpiresAt` на нового клиента
|
||||
— ротация не должна ни продлевать, ни сбрасывать оплаченный период.
|
||||
- **Создание** (`CreateVpnConfigCommandHandler`) и **ротация** (`RotateVpnConfigCommandHandler`)
|
||||
всегда передают `expiresAt: null` в `AddClientAsync` — клиент создаётся без ограничения по сроку на
|
||||
панели (`null` → `expiryTime = 0`). Биллинговым истечением этого клиента далее управляет только
|
||||
`BillingConfigResumer`/`BillingService` через `enable`; панель никогда не является источником правды
|
||||
о сроке.
|
||||
- Блокировка/разблокировка админом (`Disable()`/`Enable()`) — отдельная ось, управляет только
|
||||
`enable`, `expiresAt: null` (не трогает срок оплаты). Переименование (`EditVpnConfigCommandHandler`)
|
||||
— `enable`/`expiresAt: null` (раньше по ошибке форсировало `enable:true`, тем самым молча снимая
|
||||
|
||||
Reference in New Issue
Block a user