Implement extension request and gift functionalities in billing system
CI / Backend (build + test) (push) Successful in 1m27s
CI / Frontend (lint + typecheck + build) (push) Successful in 33s

- 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:
Leonid Pershin
2026-07-19 05:30:11 +03:00
parent e088e302e9
commit 24cee9bb78
46 changed files with 2541 additions and 78 deletions
+28 -9
View File
@@ -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) и