- 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.
93 lines
4.3 KiB
C#
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);
|
|
}
|
|
}
|