Refactor payment request handling to support role change top-ups
- Updated the `PaymentRequest` model to include a new `Kind` property, distinguishing between `Subscription` and `RoleChangeTopUp` requests. - Modified the `TelegramNotifier` to accommodate the new request type, ensuring accurate notifications for role change top-ups. - Enhanced the `ConfirmPaymentRequestCommandHandler` to handle role change top-ups without extending the billing period, reflecting the new payment logic. - Updated various application components and tests to support the new payment request structure and ensure proper functionality. - Revised API documentation to clarify the behavior of role change top-ups and their impact on billing.
This commit is contained in:
+52
-12
@@ -417,33 +417,73 @@ Singleton (как `PricingSettings`) — реквизиты для оплаты
|
||||
|
||||
### PaymentRequest — заявка на оплату
|
||||
Пользователь оформляет заявку на период (3/6/12 мес); решает админ на сайте или в Telegram. Не более
|
||||
одной активной (`AwaitingPayment`/`AwaitingConfirmation`) заявки на пользователя — инвариант
|
||||
проверяется в `CreatePaymentRequestCommandHandler`.
|
||||
одной активной (`AwaitingPayment`/`AwaitingConfirmation`) заявки **`Kind.Subscription`** на
|
||||
пользователя — инвариант проверяется в `CreatePaymentRequestCommandHandler` и не распространяется на
|
||||
`Kind.RoleChangeTopUp` (см. ниже) — доплата не должна мешать оформить/продлить обычную подписку.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------------ | ----------------------- | ------------------------------------------------------------ |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `UserId` | `Guid` | FK → AppUser (заявитель) |
|
||||
| `Period` | `PaymentPeriod` | `Quarter` (3 мес) / `HalfYear` (6 мес) / `Year` (12 мес) |
|
||||
| `AmountSnapshot` | `int` | Сумма, замороженная на момент создания: `ставка PricingSettings за период × MaxConfigs роли × число месяцев`, затем скидка по лесенке `PricingDiscountTier` (см. выше), если применима. Последующее изменение прайса/лесенки админом не меняет уже созданные заявки |
|
||||
| `Kind` | `PaymentRequestKind` | `Subscription` (оплата за период) / `RoleChangeTopUp` (доплата за апгрейд роли, см. ниже) |
|
||||
| `Period` | `PaymentPeriod?` | `Quarter` (3 мес) / `HalfYear` (6 мес) / `Year` (12 мес). `null` для `Kind.RoleChangeTopUp` — доплата не привязана к тарифному периоду |
|
||||
| `AmountSnapshot` | `int` | Сумма, замороженная на момент создания. Для `Subscription`: `ставка PricingSettings за период × MaxConfigs роли × число месяцев`, затем скидка по лесенке `PricingDiscountTier` (см. выше), если применима. Для `RoleChangeTopUp`: см. `RoleChangeTopUp.Compute` ниже. Последующее изменение прайса/лесенки админом не меняет уже созданные заявки |
|
||||
| `Status` | `PaymentRequestStatus` | `AwaitingPayment` → `AwaitingConfirmation` → `Confirmed`/`Rejected`, либо `Cancelled` из `AwaitingPayment` |
|
||||
| `DecidedBy`/`DecidedAt`/`RejectionReason` | | Кто/когда решил, причина отказа (опционально) |
|
||||
| `CreatedAt` | `DateTimeOffset` | |
|
||||
|
||||
Роль с `MaxConfigs = -1` (unlimited) не поддерживает биллинг по формуле —
|
||||
`CreatePaymentRequestCommandHandler` отдаёт `BillingErrors.UnlimitedRoleNotSupported`.
|
||||
`CreatePaymentRequestCommandHandler` отдаёт `BillingErrors.UnlimitedRoleNotSupported`; та же логика в
|
||||
`RoleChangeTopUp.Compute` (`null`, доплата не считается).
|
||||
|
||||
**Переходы** (`backend/src/PnvPanel.Domain/Billing/PaymentRequest.cs`):
|
||||
- `Create(userId, period, amount)` → `AwaitingPayment`, показываются реквизиты `BillingSettings`.
|
||||
Пользователь может `Cancel()` (только из `AwaitingPayment`) или дождаться проверки.
|
||||
- `Create(userId, period, amount)` (`Kind.Subscription`) / `CreateRoleChangeTopUp(userId, amount)`
|
||||
(`Kind.RoleChangeTopUp`) → `AwaitingPayment`, показываются реквизиты `BillingSettings`. Пользователь
|
||||
может `Cancel()` (только из `AwaitingPayment`) или дождаться проверки.
|
||||
- `MarkPaymentSent()` → пользователь нажал «Я оплатил»; `AwaitingPayment → AwaitingConfirmation`,
|
||||
админам уходит Telegram-уведомление с инлайн-кнопками `pay:approve:{id}`/`pay:reject:{id}`.
|
||||
админам уходит Telegram-уведомление с инлайн-кнопками `pay:approve:{id}`/`pay:reject:{id}` (текст
|
||||
уведомления зависит от `Kind` — период или «доплата за смену роли», см. `TelegramNotifier`).
|
||||
- `Confirm(adminId)`/`Reject(adminId, reason)` → допустимы из **обоих** `AwaitingPayment` и
|
||||
`AwaitingConfirmation` (админ мог заметить оплату раньше, чем пользователь нажал кнопку).
|
||||
`Confirm` продлевает `AppUser.BillingPaidUntil = max(текущий, сейчас) + период` (не теряет уже
|
||||
оплаченный остаток при досрочной оплате), возвращает в `Active` конфиги, приостановленные за
|
||||
неуплату (`Suspend()`/`Resume()` на `VpnConfig`, статус `Expired`), обновляет `ExpiresAt` на всех
|
||||
конфигах пользователя.
|
||||
Для `Kind.Subscription` `Confirm` продлевает `AppUser.BillingPaidUntil = max(текущий, сейчас) +
|
||||
период` (не теряет уже оплаченный остаток при досрочной оплате), возвращает в `Active` конфиги,
|
||||
приостановленные за неуплату (`Suspend()`/`Resume()` на `VpnConfig`, статус `Expired`), обновляет
|
||||
`ExpiresAt` на всех конфигах пользователя. Для `Kind.RoleChangeTopUp` `Confirm` **только** переводит
|
||||
заявку в `Confirmed` — `BillingPaidUntil` не трогает (это не покупка времени, а закрытие долга за уже
|
||||
выданный апгрейд) — см. `ConfirmPaymentRequestCommandHandler`.
|
||||
|
||||
#### RoleChangeTopUp — доплата при апгрейде роли с активным периодом
|
||||
|
||||
Пользователь с активным `BillingPaidUntil` меняет роль (тикетом `SupportTicket.RoleRequest`,
|
||||
`ApproveRoleRequestCommandHandler`) на более дорогую — по-хорошему должен доплатить разницу, а не
|
||||
доиграть апгрейд бесплатно до конца уже оплаченного срока. **Роль меняется сразу** (не блокируется
|
||||
ожиданием оплаты); доплата решается отдельно через обычный флоу `PaymentRequest`
|
||||
(`Kind.RoleChangeTopUp`) — тем же путём, что и обычная оплата: сайт (`billing.tsx`,
|
||||
`PaymentRequestPanel`) или Telegram (`pay:approve`/`pay:reject`).
|
||||
|
||||
Сумма — `RoleChangeTopUp.Compute` (`backend/src/PnvPanel.Domain/Billing/RoleChangeTopUp.cs`), чистая
|
||||
функция без I/O:
|
||||
1. Месячная стоимость роли = `PricingSettings.PricePerConfigPerQuarter × MaxConfigs`, затем скидка по
|
||||
лесенке `PricingDiscountTier` (`PricingDiscount.ResolvePercent`/`Apply`) — та же формула и тот же
|
||||
базовый (квартальный/минимальный) тариф, что у обычной оплаты, независимо от того, за какой период
|
||||
пользователь платил на самом деле — упрощение, чтобы не вводить отдельное понятие «дневная ставка
|
||||
по фактическому тарифу».
|
||||
2. Разница месячных стоимостей новой и старой роли, поделённая на 30 (условный «месяц» для
|
||||
проратирования) и умноженная на число оставшихся до `BillingPaidUntil` дней — округление до целого
|
||||
рубля (`MidpointRounding.AwayFromZero`).
|
||||
3. `null` (доплата не создаётся), если: новая роль не дороже старой (в т.ч. понижение — остаётся
|
||||
грандфазеринг, без доплаты и без возврата), оплаченный период уже истёк, либо старая/новая роль без
|
||||
лимита конфигов (`MaxConfigs = -1`, цена не считается).
|
||||
|
||||
Применяется только к самостоятельной заявке на роль (`ApproveRoleRequestCommandHandler`) — админская
|
||||
прямая смена роли (`PATCH /api/admin/users/{id}/role`, `UserManageDialog`) доплату не создаёт: это
|
||||
осознанный инструмент админа, который может быть применён как поощрение.
|
||||
|
||||
**Известное ограничение**: `GetMyBillingStatusQueryHandler` отдаёт только одну `activeRequest` —
|
||||
если у пользователя одновременно есть активная `Subscription`-заявка и `RoleChangeTopUp` (редкий
|
||||
случай: роль сменили, пока уже шла обычная оплата), на странице `/billing` будет видна только одна из
|
||||
них (обе видны в админке и обе решаемы через Telegram). Не устранено — узкий edge case, не блокирует
|
||||
основной сценарий.
|
||||
|
||||
### BillingService — приостановка за неуплату (фоновая джоба)
|
||||
`Infrastructure/BackgroundJobs/BillingService.cs`, раз в час (по образцу `TrafficSyncService`). Для
|
||||
|
||||
Reference in New Issue
Block a user