using NSubstitute; using PnvPanel.Application.Billing; using PnvPanel.Application.Billing.CreatePaymentRequest; using PnvPanel.Application.Common.Interfaces; using PnvPanel.Application.Common.Models; using PnvPanel.Application.Tests.TestSupport; using PnvPanel.Domain.Billing; using PnvPanel.Domain.Pricing; using Xunit; namespace PnvPanel.Application.Tests.Billing.CreatePaymentRequest; public class CreatePaymentRequestCommandHandlerTests { private readonly IIdentityService _identityService = Substitute.For(); private static CurrentUserProfile Profile( Guid userId, bool billingEnabled = true, int maxConfigs = 5, DateTimeOffset? paidUntil = null ) => new( userId, "alice", Guid.NewGuid(), "premium", IsActivated: true, IsBlocked: false, MaxConfigs: maxConfigs, MaxIpLimit: 3, SubscriptionToken: "sub-token", BillingEnabled: billingEnabled, BillingPaidUntil: paidUntil, BillingSuspended: false ); [Fact] public async Task Handle_WithConfiguredPricing_ComputesAmountAndCreatesRequest() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); var pricing = PricingSettings.CreateDefault(); pricing.Update(500, 450, 400); dbContext.PricingSettings.Add(pricing); await dbContext.SaveChangesAsync(CancellationToken.None); _identityService .GetProfileAsync(userId, Arg.Any()) .Returns(Profile(userId, maxConfigs: 5)); var handler = new CreatePaymentRequestCommandHandler( dbContext, _identityService, FakeCurrentUser.Authenticated(userId) ); var result = await handler.Handle( new CreatePaymentRequestCommand(PaymentPeriod.Quarter), CancellationToken.None ); Assert.True(result.IsSuccess); Assert.Equal(500 * 5 * 3, result.Value.AmountSnapshot); Assert.Equal(PaymentRequestStatus.AwaitingPayment, result.Value.Status); } [Fact] public async Task Handle_WithApplicableDiscountTier_AppliesDiscountToAmount() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); var pricing = PricingSettings.CreateDefault(); pricing.Update(500, 450, 400); dbContext.PricingSettings.Add(pricing); dbContext.PricingDiscountTiers.Add(PricingDiscountTier.Create(pricing.Id, minConfigs: 3, discountPercent: 10)); dbContext.PricingDiscountTiers.Add(PricingDiscountTier.Create(pricing.Id, minConfigs: 6, discountPercent: 20)); await dbContext.SaveChangesAsync(CancellationToken.None); _identityService .GetProfileAsync(userId, Arg.Any()) .Returns(Profile(userId, maxConfigs: 5)); var handler = new CreatePaymentRequestCommandHandler( dbContext, _identityService, FakeCurrentUser.Authenticated(userId) ); var result = await handler.Handle( new CreatePaymentRequestCommand(PaymentPeriod.Quarter), CancellationToken.None ); Assert.True(result.IsSuccess); // 500 * 5 * 3 = 7500, скидка 10% (порог 6 не достигнут при 5 конфигах) → 6750. Assert.Equal(6750, result.Value.AmountSnapshot); } [Fact] public async Task Handle_WhenBillingNotEnabled_ReturnsNotEnabled() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); _identityService .GetProfileAsync(userId, Arg.Any()) .Returns(Profile(userId, billingEnabled: false)); var handler = new CreatePaymentRequestCommandHandler( dbContext, _identityService, FakeCurrentUser.Authenticated(userId) ); var result = await handler.Handle( new CreatePaymentRequestCommand(PaymentPeriod.Quarter), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(BillingErrors.NotEnabled, result.Error); } [Fact] public async Task Handle_WhenRoleHasUnlimitedConfigs_ReturnsUnlimitedRoleNotSupported() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); _identityService .GetProfileAsync(userId, Arg.Any()) .Returns(Profile(userId, maxConfigs: RoleQuota.Unlimited)); var handler = new CreatePaymentRequestCommandHandler( dbContext, _identityService, FakeCurrentUser.Authenticated(userId) ); var result = await handler.Handle( new CreatePaymentRequestCommand(PaymentPeriod.Quarter), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(BillingErrors.UnlimitedRoleNotSupported, result.Error); } [Fact] public async Task Handle_WhenActiveRequestExists_ReturnsActiveRequestExists() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); dbContext.PaymentRequests.Add(PaymentRequest.Create(userId, PaymentPeriod.Quarter, 1000)); await dbContext.SaveChangesAsync(CancellationToken.None); _identityService .GetProfileAsync(userId, Arg.Any()) .Returns(Profile(userId)); var handler = new CreatePaymentRequestCommandHandler( dbContext, _identityService, FakeCurrentUser.Authenticated(userId) ); var result = await handler.Handle( new CreatePaymentRequestCommand(PaymentPeriod.Quarter), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(BillingErrors.ActiveRequestExists, result.Error); } [Fact] public async Task Handle_WhenPricingNotConfigured_ReturnsPricingNotConfigured() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); _identityService .GetProfileAsync(userId, Arg.Any()) .Returns(Profile(userId)); var handler = new CreatePaymentRequestCommandHandler( dbContext, _identityService, FakeCurrentUser.Authenticated(userId) ); var result = await handler.Handle( new CreatePaymentRequestCommand(PaymentPeriod.Quarter), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(BillingErrors.PricingNotConfigured, result.Error); } }