Refactor payment request handling to support role change top-ups
CI / Backend (build + test) (push) Successful in 1m19s
CI / Frontend (lint + typecheck + build) (push) Successful in 33s

- 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:
Leonid Pershin
2026-07-19 16:45:17 +03:00
parent 0dcaf1203f
commit 5ff5224935
27 changed files with 1656 additions and 76 deletions
@@ -32,9 +32,12 @@ public sealed class CreatePaymentRequestCommandHandler(
if (profile.MaxConfigs == RoleQuota.Unlimited)
return Result.Failure<PaymentRequestDto>(BillingErrors.UnlimitedRoleNotSupported);
// RoleChangeTopUp не считается активной подписной заявкой — доплата за смену роли не должна
// мешать пользователю продлить/оформить обычную подписку.
var hasActiveRequest = await dbContext.PaymentRequests.AnyAsync(
r =>
r.UserId == userId
&& r.Kind == PaymentRequestKind.Subscription
&& (
r.Status == PaymentRequestStatus.AwaitingPayment
|| r.Status == PaymentRequestStatus.AwaitingConfirmation
@@ -35,6 +35,7 @@ public sealed class MarkPaymentSentCommandHandler(
await telegramNotifier.NotifyAdminsPaymentRequestedAsync(
request.Id,
profile?.UserName ?? userId.ToString(),
request.Kind,
request.Period,
request.AmountSnapshot,
cancellationToken
@@ -4,12 +4,13 @@ namespace PnvPanel.Application.Billing;
public sealed record PaymentRequestDto(
Guid Id,
PaymentPeriod Period,
PaymentRequestKind Kind,
PaymentPeriod? Period,
int AmountSnapshot,
PaymentRequestStatus Status,
DateTimeOffset CreatedAt
)
{
public static PaymentRequestDto FromDomain(PaymentRequest request) =>
new(request.Id, request.Period, request.AmountSnapshot, request.Status, request.CreatedAt);
new(request.Id, request.Kind, request.Period, request.AmountSnapshot, request.Status, request.CreatedAt);
}
@@ -38,13 +38,16 @@ public sealed class SendRequisitesToTelegramCommandHandler(
.FirstOrDefaultAsync(cancellationToken);
var text =
$"💳 Реквизиты для оплаты ({PeriodLabel(request.Period)}, {request.AmountSnapshot} ₽):\n{settings?.RequisitesText}";
$"💳 Реквизиты для оплаты ({DescribeRequest(request)}, {request.AmountSnapshot} ₽):\n{settings?.RequisitesText}";
await telegramNotifier.NotifyUserAsync(userId, text, null, cancellationToken);
return Result.Success();
}
private static string DescribeRequest(PaymentRequest request) =>
request.Kind == PaymentRequestKind.RoleChangeTopUp ? "доплата за смену роли" : PeriodLabel(request.Period!.Value);
private static string PeriodLabel(PaymentPeriod period) =>
period switch
{