- 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.
98 lines
3.4 KiB
C#
98 lines
3.4 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using PnvPanel.Application.Auth;
|
||
using PnvPanel.Application.Billing;
|
||
using PnvPanel.Application.Common.Interfaces;
|
||
using PnvPanel.Application.Common.Messaging;
|
||
using PnvPanel.Application.Common.Models;
|
||
using PnvPanel.Domain.Audit;
|
||
using PnvPanel.Domain.Billing;
|
||
|
||
namespace PnvPanel.Application.Admin.Billing;
|
||
|
||
/// <summary>
|
||
/// Продлевает оплату (max(текущий PaidUntil, сейчас) + период), возвращает в Active конфиги,
|
||
/// приостановленные за неуплату (Expired), и синхронизирует ExpiresAt на все конфиги пользователя —
|
||
/// зеркало UnblockUserCommandHandler, но по статусу Expired (биллинг), а не Disabled (блокировка).
|
||
/// </summary>
|
||
public sealed class ConfirmPaymentRequestCommandHandler(
|
||
IAppDbContext dbContext,
|
||
IIdentityService identityService,
|
||
IXuiPanelGateway gateway,
|
||
IRealtimeNotifier notifier,
|
||
ITelegramNotifier telegramNotifier,
|
||
ICurrentUser currentUser,
|
||
ILogger<ConfirmPaymentRequestCommandHandler> logger
|
||
) : ICommandHandler<ConfirmPaymentRequestCommand, Result>
|
||
{
|
||
public async Task<Result> Handle(
|
||
ConfirmPaymentRequestCommand command,
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
if (currentUser.UserId is not { } adminId)
|
||
return Result.Failure(AuthErrors.Unauthorized);
|
||
|
||
var request = await dbContext.PaymentRequests.FirstOrDefaultAsync(
|
||
r => r.Id == command.RequestId,
|
||
cancellationToken
|
||
);
|
||
if (request is null)
|
||
return Result.Failure(BillingErrors.RequestNotFound);
|
||
|
||
if (
|
||
request.Status
|
||
is not (PaymentRequestStatus.AwaitingPayment or PaymentRequestStatus.AwaitingConfirmation)
|
||
)
|
||
return Result.Failure(BillingErrors.RequestNotDecidable);
|
||
|
||
var profile = await identityService.GetProfileAsync(request.UserId, cancellationToken);
|
||
if (profile is null)
|
||
return Result.Failure(AuthErrors.Unauthorized);
|
||
|
||
var now = DateTimeOffset.UtcNow;
|
||
var baseline = profile.BillingPaidUntil is { } paidUntil && paidUntil > now ? paidUntil : now;
|
||
var newPaidUntil = baseline.AddMonths(request.Period.ToMonths());
|
||
|
||
var extendResult = await identityService.ExtendBillingPaidUntilAsync(
|
||
request.UserId,
|
||
newPaidUntil,
|
||
cancellationToken
|
||
);
|
||
if (!extendResult.IsSuccess)
|
||
return extendResult;
|
||
|
||
request.Confirm(adminId);
|
||
|
||
await BillingConfigResumer.ResumeConfigsAsync(
|
||
dbContext,
|
||
gateway,
|
||
notifier,
|
||
logger,
|
||
request.UserId,
|
||
newPaidUntil,
|
||
cancellationToken
|
||
);
|
||
|
||
dbContext.AuditLogs.Add(
|
||
AuditLog.Create(
|
||
adminId,
|
||
"PaymentConfirmed",
|
||
"PaymentRequest",
|
||
request.Id.ToString(),
|
||
metadata: null,
|
||
AuditSource.Web
|
||
)
|
||
);
|
||
|
||
await telegramNotifier.NotifyUserAsync(
|
||
request.UserId,
|
||
$"✅ Оплата подтверждена. Доступ продлён до {newPaidUntil:dd.MM.yyyy}.",
|
||
null,
|
||
cancellationToken
|
||
);
|
||
|
||
return Result.Success();
|
||
}
|
||
}
|