Implement billing functionality and enhance role management
- Introduced billing capabilities, allowing users to request payments for subscription periods (3/6/12 months) with admin approval via Telegram. - Updated role management to include a `BillingEnabled` property, preventing billing for admin roles. - Enhanced the `CreateRoleCommand` and `UpdateRoleCommand` to accept billing parameters, ensuring proper handling during role creation and updates. - Added new endpoints for billing management and integrated billing checks into VPN config creation to enforce payment requirements. - Updated related services, models, and tests to support the new billing features, ensuring comprehensive coverage and functionality. - Enhanced documentation to reflect the new billing processes and role management changes.
This commit is contained in:
+97
-9
@@ -4,11 +4,14 @@
|
||||
`AppUser`/`AppRole` — часть Identity (живут в `Infrastructure`, т.к. расширяют `IdentityUser<Guid>`/
|
||||
`IdentityRole<Guid>`); чистый `PnvPanel.Domain` ссылается на пользователя/роль только по `Guid`.
|
||||
|
||||
Тарифы `Plan` и лимиты трафика на конфиг (`TrafficLimit`) не реализованы — единственная квота:
|
||||
число активных конфигов на роль (`AppRole.MaxConfigs`). Есть глобальная справочная цена за один
|
||||
конфиг (`PricingSettings`; редактирует только `admin`, но справочно видна и активированным пользователям
|
||||
в заявке на роль) — это не биллинг: без статусов оплаты, дат окончания
|
||||
и интеграций с платёжными системами, см. ниже.
|
||||
Лимиты трафика на конфиг (`TrafficLimit`) не реализованы — квота на число активных конфигов —
|
||||
только через `AppRole.MaxConfigs`. Есть глобальная справочная цена за один конфиг (`PricingSettings`;
|
||||
редактирует только `admin`, но справочно видна и активированным пользователям в заявке на роль) —
|
||||
используется и биллингом (см. ниже) для расчёта суммы заявки на оплату.
|
||||
|
||||
**Биллинг (подписка по сроку) реализован, но опционален и включается per-роль**
|
||||
(`AppRole.BillingEnabled`, недоступен для `admin`) — см. [Billing](#billing--подписка-по-сроку).
|
||||
Роль без флага живёт как раньше, без ограничений по сроку.
|
||||
|
||||
## Диаграмма связей
|
||||
|
||||
@@ -105,7 +108,7 @@ AppUser
|
||||
| `Protocol` | `VpnProtocol` | Денормализовано с inbound |
|
||||
| `UsedUpBytes` | `long` | Синхронизируется из 3x-ui (только для отображения — лимит трафика не применяется) |
|
||||
| `UsedDownBytes` | `long` | Синхронизируется из 3x-ui |
|
||||
| `ExpiresAt` | `DateTimeOffset?`| Зарезервировано, сейчас ничего его не выставляет — конфиг живёт бессрочно |
|
||||
| `ExpiresAt` | `DateTimeOffset?`| Для billing-ролей — денормализованный `AppUser.BillingPaidUntil` (см. Billing); иначе `null`, конфиг живёт бессрочно. Уходит в `Subscription-Userinfo` для VPN-клиента |
|
||||
| `Status` | `ConfigStatus` | `Active` / `Disabled` / `Expired` / `LimitReached` / `Revoked`|
|
||||
| `SubscriptionToken`| `string` | Секрет для публичного `/sub/{token}` |
|
||||
| `LastSyncAt` | `DateTimeOffset?`| |
|
||||
@@ -122,6 +125,9 @@ AppUser
|
||||
удаляет старого, генерирует новый `SubscriptionToken`; квоту **не тратит**. Для случая утечки ссылки.
|
||||
- `Disable()`/`Enable()` → меняют только статус записи (`Active ↔ Disabled`); отключение/включение
|
||||
самого клиента в 3x-ui делает хендлер отдельным вызовом гейтвея (используется при блокировке юзера).
|
||||
- `Suspend()`/`Resume()` → меняют статус (`Active ↔ Expired`), отдельно от `Disable()`/`Enable()` —
|
||||
приостановка за неуплату (биллинг) не должна конфликтовать с блокировкой админом: разблокировка
|
||||
возвращает в `Active` только то, что было погашено именно блокировкой, и наоборот (см. Billing).
|
||||
- `Rename(label)` → юзер меняет метку (синкается в 3x-ui как имя клиента).
|
||||
- Лимит одновременных IP (`limitIp` в 3x-ui) выставляется при создании клиента (`Create`/`Rotate`) по
|
||||
квоте роли пользователя (`AppRole.MaxIpLimit`; -1 = без лимита) — панель не даёт настраивать его
|
||||
@@ -138,9 +144,9 @@ AppUser
|
||||
- Инбаунд должен быть доступен роли пользователя (`Inbound.AllowedRoles`).
|
||||
- Разрешено несколько конфигов в одном инбаунде (ограничение — только общая квота роли).
|
||||
|
||||
> Лимиты трафика и автоматическое истечение срока конфига не реализованы. `ExpiresAt` никогда не
|
||||
> выставляется; `ConfigStatus.LimitReached` в значении enum есть, но код в него никогда не переводит
|
||||
> конфиг. Квота на число конфигов реализована через `AppRole.MaxConfigs` (см. [tech-stack.md](tech-stack.md)).
|
||||
> Лимиты трафика не реализованы. `ConfigStatus.LimitReached` в значении enum есть, но код в него
|
||||
> никогда не переводит конфиг. Квота на число конфигов реализована через `AppRole.MaxConfigs` (см.
|
||||
> [tech-stack.md](tech-stack.md)). Истечение срока — только для billing-ролей, см. Billing ниже.
|
||||
|
||||
### TrafficSample — история трафика (для графиков)
|
||||
Точки потребления во времени; пишутся синхронизацией.
|
||||
@@ -290,6 +296,7 @@ UI **настойчиво напоминает** привязать его (ед
|
||||
| `MaxConfigs` | `int` | Квота активных конфигов (-1 = без лимита; для `admin` — без лимита) |
|
||||
| `MaxIpLimit` | `int` | Лимит одновременных IP на клиента (`limitIp` в 3x-ui; -1 = без лимита; для `admin` — без лимита) |
|
||||
| `IsSystem` | `bool` | Системная (`admin`, `user`) — нельзя удалить/переименовать |
|
||||
| `BillingEnabled` | `bool` | Включает биллинг для пользователей с этой ролью; нельзя включить для `admin` (см. Billing) |
|
||||
|
||||
Сидируются: `admin` (оба лимита без ограничения) и `user` (`MaxConfigs` = `Roles__DefaultUserMaxConfigs`,
|
||||
по умолчанию 3; `MaxIpLimit` = `Roles__DefaultUserMaxIpLimit`, по умолчанию 2).
|
||||
@@ -348,6 +355,87 @@ PricePerConfigPerQuarter × 3`).
|
||||
admin- и user-facing путями безопасно. Сидируется пустой строкой при старте (`IPricingSettingsSeeder`,
|
||||
если таблица пуста) и заново после полного сброса панели (см. «Полный сброс панели» выше).
|
||||
|
||||
### Billing — подписка по сроку
|
||||
|
||||
Опциональная подсистема: включается per-роль (`AppRole.BillingEnabled`), недоступна для `admin`.
|
||||
Роль без флага не затрагивается — конфиги живут бессрочно, как без биллинга вообще.
|
||||
|
||||
**`AppUser` (доп. поля, только для billing-ролей):**
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| --------------------------------- | ------------------ | ------------------------------------------------------------ |
|
||||
| `BillingPaidUntil` | `DateTimeOffset?` | Оплачено до этой даты; `null` — оплата ещё ни разу не выставлялась |
|
||||
| `BillingSuspended` | `bool` | Конфиги приостановлены за неуплату (см. `BillingService`) |
|
||||
| `BillingLastWarnedForPaidUntil` | `DateTimeOffset?` | Для какого `PaidUntil` уже отправлено предупреждение «истекает через N дней» — не даёт слать повторно на каждый тик джобы |
|
||||
|
||||
**Грейс-период**: когда пользователю впервые назначается billing-роль (или роли, где он уже состоит,
|
||||
включают `BillingEnabled`) и `BillingPaidUntil == null` — `RoleService` выставляет
|
||||
`BillingPaidUntil = now + BillingSettings.GraceDays` автоматически (`ChangeUserRoleAsync`/
|
||||
`UpdateRoleAsync`). Без этого пользователь был бы «просрочен» с первой секунды.
|
||||
|
||||
### BillingSettings — глобальные настройки биллинга
|
||||
Singleton (как `PricingSettings`) — реквизиты для оплаты и длина грейс-периода, редактирует `admin`.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ----------------- | ---------------- | ----------------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `RequisitesText` | `string` | Произвольный текст реквизитов (карта/крипто-адрес/СБП и т.д.), показывается пользователю с заявкой |
|
||||
| `GraceDays` | `int` | По умолчанию 7 (`BillingSettings.DefaultGraceDays`) |
|
||||
| `UpdatedAt` | `DateTimeOffset` | |
|
||||
|
||||
### PaymentRequest — заявка на оплату
|
||||
Пользователь оформляет заявку на период (3/6/12 мес); решает админ на сайте или в Telegram. Не более
|
||||
одной активной (`AwaitingPayment`/`AwaitingConfirmation`) заявки на пользователя — инвариант
|
||||
проверяется в `CreatePaymentRequestCommandHandler`.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------------ | ----------------------- | ------------------------------------------------------------ |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `UserId` | `Guid` | FK → AppUser (заявитель) |
|
||||
| `Period` | `PaymentPeriod` | `Quarter` (3 мес) / `HalfYear` (6 мес) / `Year` (12 мес) |
|
||||
| `AmountSnapshot` | `int` | Сумма, замороженная на момент создания: `ставка PricingSettings за период × MaxConfigs роли × число месяцев`. Последующее изменение прайса админом не меняет уже созданные заявки |
|
||||
| `Status` | `PaymentRequestStatus` | `AwaitingPayment` → `AwaitingConfirmation` → `Confirmed`/`Rejected`, либо `Cancelled` из `AwaitingPayment` |
|
||||
| `DecidedBy`/`DecidedAt`/`RejectionReason` | | Кто/когда решил, причина отказа (опционально) |
|
||||
| `CreatedAt` | `DateTimeOffset` | |
|
||||
|
||||
Роль с `MaxConfigs = -1` (unlimited) не поддерживает биллинг по формуле —
|
||||
`CreatePaymentRequestCommandHandler` отдаёт `BillingErrors.UnlimitedRoleNotSupported`.
|
||||
|
||||
**Переходы** (`backend/src/PnvPanel.Domain/Billing/PaymentRequest.cs`):
|
||||
- `Create(userId, period, amount)` → `AwaitingPayment`, показываются реквизиты `BillingSettings`.
|
||||
Пользователь может `Cancel()` (только из `AwaitingPayment`) или дождаться проверки.
|
||||
- `MarkPaymentSent()` → пользователь нажал «Я оплатил»; `AwaitingPayment → AwaitingConfirmation`,
|
||||
админам уходит Telegram-уведомление с инлайн-кнопками `pay:approve:{id}`/`pay:reject:{id}`.
|
||||
- `Confirm(adminId)`/`Reject(adminId, reason)` → допустимы из **обоих** `AwaitingPayment` и
|
||||
`AwaitingConfirmation` (админ мог заметить оплату раньше, чем пользователь нажал кнопку).
|
||||
`Confirm` продлевает `AppUser.BillingPaidUntil = max(текущий, сейчас) + период` (не теряет уже
|
||||
оплаченный остаток при досрочной оплате), возвращает в `Active` конфиги, приостановленные за
|
||||
неуплату (`Suspend()`/`Resume()` на `VpnConfig`, статус `Expired`), обновляет `ExpiresAt` на всех
|
||||
конфигах пользователя.
|
||||
|
||||
### BillingService — приостановка за неуплату (фоновая джоба)
|
||||
`Infrastructure/BackgroundJobs/BillingService.cs`, раз в час (по образцу `TrafficSyncService`). Для
|
||||
каждого пользователя с billing-ролью, не заблокированного (`IsBlocked`):
|
||||
- есть `PaymentRequest` в статусе `AwaitingConfirmation` → **пропустить** — это и есть защита «заявка
|
||||
висит на подтверждении админом, а срок истёк» из требований: конфиги не гасятся, пока админ не
|
||||
решит (не по вине пользователя, что админ не успел проверить оплату);
|
||||
- `BillingPaidUntil` в прошлом (или `null`) и ещё не `BillingSuspended` → приостановить все `Active`
|
||||
конфиги (`Suspend()` → `Expired`, гейтвей `UpdateClientAsync(enable:false)`, идемпотентно как в
|
||||
`BlockUserCommandHandler`), `AppUser.BillingSuspended = true`, Telegram-уведомление пользователю,
|
||||
`AuditLog` (`BillingSuspended`, источник `System`). На последующих тиках (уже suspended) — только
|
||||
идемпотентная досуспензия «зависших» `Active`-конфигов (самовосстановление после недоступности
|
||||
ноды), без повторных уведомлений;
|
||||
- до истечения ≤ 3 дней и предупреждение для этого `PaidUntil` ещё не отправлено
|
||||
(`BillingLastWarnedForPaidUntil != PaidUntil`) → Telegram-предупреждение, отметка отправки.
|
||||
|
||||
`CreateVpnConfigCommandHandler` дополнительно не даёт создать **новый** конфиг, если роль billing
|
||||
и оплата просрочена (`ConfigErrors.BillingRequired`) — иначе приостановку можно было бы обойти
|
||||
созданием свежего конфига.
|
||||
|
||||
`GET/POST /api/billing/*` — пользователь (статус, создание/отмена заявки, «я оплатил», отправка
|
||||
реквизитов в свой Telegram). `GET/PUT/POST /api/admin/billing/*` — админ (настройки, список заявок,
|
||||
подтверждение/отклонение), только `admin`.
|
||||
|
||||
### ActivationRequest — запрос активации
|
||||
Пользователь просит активацию у админа; админ одобряет/отклоняет на сайте или в Telegram.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user