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:
@@ -5,6 +5,7 @@ using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Application.Common.Models;
|
||||
using PnvPanel.Application.Support;
|
||||
using PnvPanel.Domain.Audit;
|
||||
using PnvPanel.Domain.Billing;
|
||||
using PnvPanel.Domain.Support;
|
||||
|
||||
namespace PnvPanel.Application.Admin.Support;
|
||||
@@ -12,6 +13,7 @@ namespace PnvPanel.Application.Admin.Support;
|
||||
public sealed class ApproveRoleRequestCommandHandler(
|
||||
IAppDbContext dbContext,
|
||||
IRoleService roleService,
|
||||
IIdentityService identityService,
|
||||
IRealtimeNotifier notifier,
|
||||
ITelegramNotifier telegramNotifier,
|
||||
ICurrentUser currentUser
|
||||
@@ -38,10 +40,19 @@ public sealed class ApproveRoleRequestCommandHandler(
|
||||
if (ticket.Status != TicketStatus.Open)
|
||||
return Result.Failure(SupportErrors.NotOpen);
|
||||
|
||||
Guid roleId;
|
||||
// Снимаем профиль ДО смены роли — нужен старый MaxConfigs/BillingPaidUntil для проратированной
|
||||
// доплаты за апгрейд (см. ниже), после ChangeUserRoleAsync эти данные уже недоступны.
|
||||
var oldProfile = await identityService.GetProfileAsync(ticket.UserId, cancellationToken);
|
||||
|
||||
RoleDto newRole;
|
||||
if (ticket.RequestedRoleId is { } existingRoleId)
|
||||
{
|
||||
roleId = existingRoleId;
|
||||
var roles = await roleService.ListRolesAsync(cancellationToken);
|
||||
var found = roles.FirstOrDefault(r => r.Id == existingRoleId);
|
||||
if (found is null)
|
||||
return Result.Failure(SupportErrors.NotFound);
|
||||
|
||||
newRole = found;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -55,12 +66,12 @@ public sealed class ApproveRoleRequestCommandHandler(
|
||||
if (!createResult.IsSuccess)
|
||||
return Result.Failure(createResult.Error);
|
||||
|
||||
roleId = createResult.Value.Id;
|
||||
newRole = createResult.Value;
|
||||
}
|
||||
|
||||
var assignResult = await roleService.ChangeUserRoleAsync(
|
||||
ticket.UserId,
|
||||
roleId,
|
||||
newRole.Id,
|
||||
cancellationToken
|
||||
);
|
||||
if (!assignResult.IsSuccess)
|
||||
@@ -87,6 +98,51 @@ public sealed class ApproveRoleRequestCommandHandler(
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (newRole.BillingEnabled && oldProfile?.BillingPaidUntil is { } paidUntil)
|
||||
await CreateTopUpIfNeededAsync(ticket.UserId, oldProfile.MaxConfigs, newRole.MaxConfigs, paidUntil, cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
/// <summary>Роль подорожала, а оплаченный период ещё активен — по-хорошему пользователь должен
|
||||
/// доплатить разницу, а не доиграть апгрейд бесплатно до конца уже оплаченного срока. Роль меняется
|
||||
/// сразу (см. выше); доплата решается отдельно через обычный флоу PaymentRequest — см.
|
||||
/// domain-model.md#rolechangetopup.</summary>
|
||||
private async Task CreateTopUpIfNeededAsync(
|
||||
Guid userId,
|
||||
int oldMaxConfigs,
|
||||
int newMaxConfigs,
|
||||
DateTimeOffset paidUntil,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var pricing = await dbContext.PricingSettings.AsNoTracking().FirstOrDefaultAsync(cancellationToken);
|
||||
if (pricing?.PricePerConfigPerQuarter is not { } rate)
|
||||
return;
|
||||
|
||||
var tiers = await dbContext
|
||||
.PricingDiscountTiers.AsNoTracking()
|
||||
.Where(t => t.PricingSettingsId == pricing.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var amount = RoleChangeTopUp.Compute(
|
||||
rate,
|
||||
oldMaxConfigs,
|
||||
newMaxConfigs,
|
||||
tiers,
|
||||
paidUntil,
|
||||
DateTimeOffset.UtcNow
|
||||
);
|
||||
if (amount is not { } topUpAmount)
|
||||
return;
|
||||
|
||||
dbContext.PaymentRequests.Add(PaymentRequest.CreateRoleChangeTopUp(userId, topUpAmount));
|
||||
|
||||
await telegramNotifier.NotifyUserAsync(
|
||||
userId,
|
||||
$"💳 Новая роль дороже прежней — требуется доплата {topUpAmount} ₽ за оставшуюся часть оплаченного периода.",
|
||||
"/billing",
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user