Implement extension request and gift functionalities in billing system
- Added new endpoints for creating and managing extension requests, allowing users to request billing period extensions. - Implemented admin approval processes for extension requests via Telegram, including inline buttons for approval and rejection. - Introduced a gifting feature for admins to grant additional billing days directly to users without a request. - Updated the support ticket model to accommodate extension requests and their associated properties. - Enhanced the Telegram notifier to inform admins of new extension requests and notify users of approval or rejection. - Updated frontend components to support the new extension request and gifting functionalities, including user interfaces for managing these features. - Revised API documentation to reflect the new endpoints and their usage in the billing context.
This commit is contained in:
+28
-9
@@ -434,7 +434,11 @@ Singleton (как `PricingSettings`) — реквизиты для оплаты
|
||||
|
||||
`GET/POST /api/billing/*` — пользователь (статус, создание/отмена заявки, «я оплатил», отправка
|
||||
реквизитов в свой Telegram). `GET/PUT/POST /api/admin/billing/*` — админ (настройки, список заявок,
|
||||
подтверждение/отклонение), только `admin`.
|
||||
подтверждение/отклонение, `POST /gift` — выдать N дней конкретному пользователю без заявки), только
|
||||
`admin`. Продление PaidUntil попадает в панель тремя путями — подтверждённая `PaymentRequest`,
|
||||
одобренная `SupportTicket(ExtensionRequest)` и прямой гифт от админа — все три используют один и тот
|
||||
же `BillingConfigResumer` (см. Application/Billing), различается только вычисление `newPaidUntil`
|
||||
(месяцы для оплаты, дни для продления/гифта) и триггер (пользователь vs админ).
|
||||
|
||||
### ActivationRequest — запрос активации
|
||||
Пользователь просит активацию у админа; админ одобряет/отклоняет на сайте или в Telegram.
|
||||
@@ -480,33 +484,44 @@ Singleton (как `PricingSettings`) — реквизиты для оплаты
|
||||
После `Consumed`/`Expired` — не переиспользуется.
|
||||
|
||||
### SupportTicket — обращение в поддержку
|
||||
Два вида: `BugReport` (свободная форма, с вложениями) и `RoleRequest` (запрос существующей роли —
|
||||
кроме `admin` — либо параметров новой). Текст/обоснование не хранится отдельным полем — это первое
|
||||
сообщение в переписке (`TicketComment`), созданное вместе с тикетом в одной операции.
|
||||
Три вида: `BugReport` (свободная форма, с вложениями), `RoleRequest` (запрос существующей роли —
|
||||
кроме `admin` — либо параметров новой) и `ExtensionRequest` (продление оплаченного периода на N
|
||||
дней — только для billing-ролей, см. Billing выше). Текст/обоснование не хранится отдельным полем —
|
||||
это первое сообщение в переписке (`TicketComment`), созданное вместе с тикетом в одной операции.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------------- | ----------------- | ---------------------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `UserId` | `Guid` | FK → AppUser (автор) |
|
||||
| `Type` | `TicketType` | `BugReport` / `RoleRequest` |
|
||||
| `Type` | `TicketType` | `BugReport` / `RoleRequest` / `ExtensionRequest` |
|
||||
| `Status` | `TicketStatus` | `Open` / `Resolved` / `Closed` |
|
||||
| `RequestedRoleId` | `Guid?` | Заполнено для `RoleRequest` при выборе существующей роли |
|
||||
| `ProposedRoleName` | `string?` | Заполнено для `RoleRequest` при запросе новой роли |
|
||||
| `ProposedMaxConfigs`| `int?` | Параметры новой роли (см. `AppRole.MaxConfigs`) |
|
||||
| `ProposedMaxIpLimit`| `int?` | Параметры новой роли (см. `AppRole.MaxIpLimit`) |
|
||||
| `RequestedDays` | `int?` | Заполнено для `ExtensionRequest` — сколько дней просит пользователь (1–365) |
|
||||
| `CreatedAt` | `DateTimeOffset` | |
|
||||
|
||||
Инварианты и переходы (`backend/src/PnvPanel.Domain/Support/SupportTicket.cs`): `RequestedRoleId`
|
||||
и `Proposed*` никогда не заполнены одновременно — гарантируется отдельными фабриками
|
||||
(`CreateRoleRequestForExistingRole`/`CreateRoleRequestForNewRole`), а не runtime-проверкой.
|
||||
(`CreateRoleRequestForExistingRole`/`CreateRoleRequestForNewRole`), а не runtime-проверкой. Аналогично
|
||||
`RequestedDays` заполняется только фабрикой `CreateExtensionRequest`.
|
||||
- `Resolve()` — только из `Open`. Для `RoleRequest` одобрение — оркестрация в Application
|
||||
(`ApproveRoleRequestCommandHandler`): при новой роли сначала `IRoleService.CreateRoleAsync`, затем
|
||||
в любом случае `ChangeUserRoleAsync` пользователю, и только потом `ticket.Resolve()`.
|
||||
- `Close()` — из `Open` или `Resolved`, **финал** (обратного пути нет). Для `RoleRequest` — отклонение.
|
||||
в любом случае `ChangeUserRoleAsync` пользователю, и только потом `ticket.Resolve()`. Для
|
||||
`ExtensionRequest` — `ApproveExtensionRequestCommandHandler` продлевает `AppUser.BillingPaidUntil`
|
||||
на `RequestedDays` (от `max(текущий, сейчас)`, как и у `PaymentRequest`) и возвращает в `Active`
|
||||
конфиги, приостановленные за неуплату (`BillingConfigResumer`, тот же helper, что и у подтверждения
|
||||
оплаты и гифт-дней от админа).
|
||||
- `Close()` — из `Open` или `Resolved`, **финал** (обратного пути нет). Для `RoleRequest`/
|
||||
`ExtensionRequest` — отклонение.
|
||||
- `Reopen()` — только из `Resolved` (владелец тикета); `Closed` не переоткрывается.
|
||||
- Одновременно не более одной **открытой** заявки на роль (`Type == RoleRequest && Status == Open`)
|
||||
и отдельно не более одной открытой заявки на продление (`Type == ExtensionRequest && Status == Open`)
|
||||
на пользователя — проверяется в Application, аналогично `ActivationRequest.AlreadyPending`.
|
||||
Баг-репорты такого ограничения не имеют.
|
||||
- Создать `ExtensionRequest` может только пользователь с billing-ролью
|
||||
(`CurrentUserProfile.BillingEnabled`) — иначе `Billing.NotEnabled`.
|
||||
- Доступ — только активированному пользователю (`IRequiresActivation`, как и у конфигов/новостей);
|
||||
админские действия (resolve/close/approve/reject) идут по отдельным `/api/admin/support/*` с
|
||||
ролевой проверкой, без завязки на активацию.
|
||||
@@ -572,7 +587,7 @@ enum TelegramLoginStatus { Pending, Approved, Rejected, Expired, Consumed }
|
||||
enum ActivationStatus { Pending, Approved, Rejected }
|
||||
enum AuditSource { Web, Telegram, System }
|
||||
enum OsPlatform { IOS, Android, Windows, MacOS, Linux }
|
||||
enum TicketType { BugReport, RoleRequest }
|
||||
enum TicketType { BugReport, RoleRequest, ExtensionRequest }
|
||||
enum TicketStatus { Open, Resolved, Closed }
|
||||
```
|
||||
|
||||
@@ -604,6 +619,10 @@ enum TicketStatus { Open, Resolved, Closed }
|
||||
| `AddTicketCommentCommandHandler` | Realtime `ticketUpdated` владельцу, только если комментирует не он сам |
|
||||
| `ApproveRoleRequestCommandHandler` | Создаёт роль (если новая) + назначает пользователю; `AuditLog` (`RoleRequestApproved`); Telegram-DM владельцу |
|
||||
| `RejectRoleRequestCommandHandler` | `AuditLog` (`RoleRequestRejected`); Telegram-DM владельцу |
|
||||
| `CreateExtensionRequestTicketCommandHandler` | Realtime `ticketCreated` группе `admins`; Telegram админам — инлайн-кнопки «Одобрить/Отклонить» |
|
||||
| `ApproveExtensionRequestCommandHandler` | Продлевает `BillingPaidUntil`, возвращает приостановленные конфиги; `AuditLog` (`ExtensionRequestApproved`); Telegram-DM владельцу |
|
||||
| `RejectExtensionRequestCommandHandler` | `AuditLog` (`ExtensionRequestRejected`); Telegram-DM владельцу |
|
||||
| `GrantBillingGiftCommandHandler` | Админ дарит N дней (`/admin/billing/gift`) — та же логика продления/возврата конфигов, что и у заявок; `AuditLog` (`BillingGiftGranted`); Telegram-DM владельцу |
|
||||
| `ResolveTicketCommandHandler` / `CloseTicketCommandHandler` | `AuditLog` (`TicketResolved`/`TicketClosed`); realtime `ticketUpdated` владельцу |
|
||||
|
||||
SignalR-события и группы — см. [architecture.md](architecture.md#realtime-signalr) и
|
||||
|
||||
Reference in New Issue
Block a user