Files
PnvPanel/backend/src/PnvPanel.Application/Billing/CreatePaymentRequest/CreatePaymentRequestCommandHandler.cs
T
Leonid Pershin fad03c2834
CI / Backend (build + test) (push) Failing after 1m23s
CI / Frontend (lint + typecheck + build) (push) Successful in 34s
Enhance user plan management and update related endpoints
- Added new configuration options for user plans in `.env.example`, including `Plans__MaxCustomConfigCount` and `Plans__MinCustomConfigCount`.
- Introduced `MapPlanEndpoints` in `Program.cs` to handle plan-related API routes.
- Implemented `SetUserPlan` endpoint in `RoleEndpoints` to allow admins to assign plans to users.
- Removed deprecated role request approval endpoints from `AdminSupportEndpoints`.
- Updated `ITelegramNotifier` and related classes to reflect changes in role request handling and payment notifications.
- Refactored role management commands to remove `MaxConfigs` and focus on `MaxIpLimit` and billing settings.
- Enhanced billing request handling to accommodate plan changes instead of role changes.
- Updated various interfaces and command handlers to support new plan management features.
2026-07-23 22:52:20 +03:00

93 lines
4.3 KiB
C#

using Microsoft.EntityFrameworkCore;
using PnvPanel.Application.Auth;
using PnvPanel.Application.Common.Concurrency;
using PnvPanel.Application.Common.Interfaces;
using PnvPanel.Application.Common.Messaging;
using PnvPanel.Application.Common.Models;
using PnvPanel.Domain.Billing;
using PnvPanel.Domain.Pricing;
namespace PnvPanel.Application.Billing.CreatePaymentRequest;
/// <summary>Проверка "нет активной заявки" + создание — под AdvisoryLock (по UserId): без неё
/// два одновременных запроса (двойной клик, повтор при таймауте) оба могли бы пройти проверку и
/// создать по заявке каждый — а дальше их обе можно независимо подтвердить, продлив оплату дважды.</summary>
public sealed class CreatePaymentRequestCommandHandler(
IAppDbContext dbContext,
IIdentityService identityService,
ICurrentUser currentUser
) : ICommandHandler<CreatePaymentRequestCommand, Result<PaymentRequestDto>>
{
public async Task<Result<PaymentRequestDto>> Handle(
CreatePaymentRequestCommand command,
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure<PaymentRequestDto>(AuthErrors.Unauthorized);
var profile = await identityService.GetProfileAsync(userId, cancellationToken);
if (profile is null)
return Result.Failure<PaymentRequestDto>(AuthErrors.Unauthorized);
if (!profile.BillingEnabled)
return Result.Failure<PaymentRequestDto>(BillingErrors.NotEnabled);
if (profile.ConfigQuota == RoleQuota.Unlimited)
return Result.Failure<PaymentRequestDto>(BillingErrors.UnlimitedRoleNotSupported);
var pricing = await dbContext.PricingSettings.AsNoTracking().FirstOrDefaultAsync(cancellationToken);
if (pricing is null)
return Result.Failure<PaymentRequestDto>(BillingErrors.PricingNotConfigured);
var ratePerMonth = command.Period switch
{
PaymentPeriod.Quarter => pricing.PricePerConfigPerQuarter,
PaymentPeriod.HalfYear => pricing.PricePerConfigPerHalfYear,
PaymentPeriod.Year => pricing.PricePerConfigPerYear,
_ => null,
};
if (ratePerMonth is not { } rate)
return Result.Failure<PaymentRequestDto>(BillingErrors.PricingNotConfigured);
var discountTiers = await dbContext
.PricingDiscountTiers.AsNoTracking()
.Where(t => t.PricingSettingsId == pricing.Id)
.ToListAsync(cancellationToken);
var discountPercent = PricingDiscount.ResolvePercent(discountTiers, profile.ConfigQuota);
var amount = PricingDiscount.Apply(rate * profile.ConfigQuota * command.Period.ToMonths(), discountPercent);
var claimed = await AdvisoryLock.RunAsync(
dbContext,
userId,
async lockedCancellationToken =>
{
// PlanChangeTopUp не считается активной подписной заявкой — доплата за смену тарифа не
// должна мешать пользователю продлить/оформить обычную подписку.
var hasActiveRequest = await dbContext.PaymentRequests.AnyAsync(
r =>
r.UserId == userId
&& r.Kind == PaymentRequestKind.Subscription
&& (
r.Status == PaymentRequestStatus.AwaitingPayment
|| r.Status == PaymentRequestStatus.AwaitingConfirmation
),
lockedCancellationToken
);
if (hasActiveRequest)
return Result.Failure<PaymentRequest>(BillingErrors.ActiveRequestExists);
var request = PaymentRequest.Create(userId, command.Period, amount);
dbContext.PaymentRequests.Add(request);
return Result.Success(request);
},
cancellationToken
);
return claimed.IsSuccess
? Result.Success(PaymentRequestDto.FromDomain(claimed.Value))
: Result.Failure<PaymentRequestDto>(claimed.Error);
}
}