using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Http.HttpResults; using NSubstitute; using NSubstitute.ReturnsExtensions; using Stripe; using Xunit; namespace Bit.Api.Test.Billing.Controllers; [ControllerCustomize(typeof(ProviderBillingController))] [SutProviderCustomize] public class ProviderBillingControllerTests { #region GetSubscriptionAsync [Theory, BitAutoData] public async Task GetSubscriptionAsync_FFDisabled_NotFound( Guid providerId, SutProvider sutProvider) { sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) .Returns(false); var result = await sutProvider.Sut.GetSubscriptionAsync(providerId); Assert.IsType(result); } [Theory, BitAutoData] public async Task GetSubscriptionAsync_NullProvider_NotFound( Guid providerId, SutProvider sutProvider) { sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) .Returns(true); sutProvider.GetDependency().GetByIdAsync(providerId).ReturnsNull(); var result = await sutProvider.Sut.GetSubscriptionAsync(providerId); Assert.IsType(result); } [Theory, BitAutoData] public async Task GetSubscriptionAsync_NotProviderAdmin_Unauthorized( Provider provider, SutProvider sutProvider) { sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) .Returns(true); sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency().ProviderProviderAdmin(provider.Id) .Returns(false); var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); Assert.IsType(result); } [Theory, BitAutoData] public async Task GetSubscriptionAsync_ProviderNotBillable_Unauthorized( Provider provider, SutProvider sutProvider) { sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) .Returns(true); provider.Type = ProviderType.Reseller; provider.Status = ProviderStatusType.Created; sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency().ProviderProviderAdmin(provider.Id) .Returns(false); var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); Assert.IsType(result); } [Theory, BitAutoData] public async Task GetSubscriptionAsync_NullConsolidatedBillingSubscription_NotFound( Provider provider, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); sutProvider.GetDependency().GetConsolidatedBillingSubscription(provider).ReturnsNull(); var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); Assert.IsType(result); } [Theory, BitAutoData] public async Task GetSubscriptionAsync_Ok( Provider provider, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); var configuredProviderPlans = new List { new (Guid.NewGuid(), provider.Id, PlanType.TeamsMonthly, 50, 10, 30), new (Guid.NewGuid(), provider.Id , PlanType.EnterpriseMonthly, 100, 0, 90) }; var subscription = new Subscription { Status = "active", CurrentPeriodEnd = new DateTime(2025, 1, 1), Customer = new Customer { Discount = new Discount { Coupon = new Coupon { PercentOff = 10 } } } }; var consolidatedBillingSubscription = new ConsolidatedBillingSubscriptionDTO( configuredProviderPlans, subscription); sutProvider.GetDependency().GetConsolidatedBillingSubscription(provider) .Returns(consolidatedBillingSubscription); var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); Assert.IsType>(result); var response = ((Ok)result).Value; Assert.Equal(response.Status, subscription.Status); Assert.Equal(response.CurrentPeriodEndDate, subscription.CurrentPeriodEnd); Assert.Equal(response.DiscountPercentage, subscription.Customer!.Discount!.Coupon!.PercentOff); var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var providerTeamsPlan = response.Plans.FirstOrDefault(plan => plan.PlanName == teamsPlan.Name); Assert.NotNull(providerTeamsPlan); Assert.Equal(50, providerTeamsPlan.SeatMinimum); Assert.Equal(10, providerTeamsPlan.PurchasedSeats); Assert.Equal(30, providerTeamsPlan.AssignedSeats); Assert.Equal(60 * teamsPlan.PasswordManager.SeatPrice, providerTeamsPlan.Cost); Assert.Equal("Monthly", providerTeamsPlan.Cadence); var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); var providerEnterprisePlan = response.Plans.FirstOrDefault(plan => plan.PlanName == enterprisePlan.Name); Assert.NotNull(providerEnterprisePlan); Assert.Equal(100, providerEnterprisePlan.SeatMinimum); Assert.Equal(0, providerEnterprisePlan.PurchasedSeats); Assert.Equal(90, providerEnterprisePlan.AssignedSeats); Assert.Equal(100 * enterprisePlan.PasswordManager.SeatPrice, providerEnterprisePlan.Cost); Assert.Equal("Monthly", providerEnterprisePlan.Cadence); } #endregion #region GetPaymentInformationAsync [Theory, BitAutoData] public async Task GetPaymentInformation_PaymentInformationNull_NotFound( Provider provider, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); sutProvider.GetDependency().GetPaymentInformation(provider).ReturnsNull(); var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); Assert.IsType(result); } [Theory, BitAutoData] public async Task GetPaymentInformation_Ok( Provider provider, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); var maskedPaymentMethod = new MaskedPaymentMethodDTO(PaymentMethodType.Card, "VISA *1234", false); var taxInformation = new TaxInformationDTO("US", "12345", "123456789", "123 Example St.", null, "Example Town", "NY"); sutProvider.GetDependency().GetPaymentInformation(provider).Returns(new PaymentInformationDTO( 100, maskedPaymentMethod, taxInformation)); var result = await sutProvider.Sut.GetPaymentInformationAsync(provider.Id); Assert.IsType>(result); var response = ((Ok)result).Value; Assert.Equal(100, response.AccountCredit); Assert.Equal(maskedPaymentMethod.Description, response.PaymentMethod.Description); Assert.Equal(taxInformation.TaxId, response.TaxInformation.TaxId); } #endregion #region GetPaymentMethodAsync [Theory, BitAutoData] public async Task GetPaymentMethod_PaymentMethodNull_NotFound( Provider provider, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); sutProvider.GetDependency().GetPaymentMethod(provider).ReturnsNull(); var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); Assert.IsType(result); } [Theory, BitAutoData] public async Task GetPaymentMethod_Ok( Provider provider, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); sutProvider.GetDependency().GetPaymentMethod(provider).Returns(new MaskedPaymentMethodDTO( PaymentMethodType.Card, "Description", false)); var result = await sutProvider.Sut.GetPaymentMethodAsync(provider.Id); Assert.IsType>(result); var response = ((Ok)result).Value; Assert.Equal(PaymentMethodType.Card, response.Type); Assert.Equal("Description", response.Description); Assert.False(response.NeedsVerification); } #endregion #region GetTaxInformationAsync [Theory, BitAutoData] public async Task GetTaxInformation_TaxInformationNull_NotFound( Provider provider, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); sutProvider.GetDependency().GetTaxInformation(provider).ReturnsNull(); var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); Assert.IsType(result); } [Theory, BitAutoData] public async Task GetTaxInformation_Ok( Provider provider, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); sutProvider.GetDependency().GetTaxInformation(provider).Returns(new TaxInformationDTO( "US", "12345", "123456789", "123 Example St.", null, "Example Town", "NY")); var result = await sutProvider.Sut.GetTaxInformationAsync(provider.Id); Assert.IsType>(result); var response = ((Ok)result).Value; Assert.Equal("US", response.Country); Assert.Equal("12345", response.PostalCode); Assert.Equal("123456789", response.TaxId); Assert.Equal("123 Example St.", response.Line1); Assert.Null(response.Line2); Assert.Equal("Example Town", response.City); Assert.Equal("NY", response.State); } #endregion #region UpdatePaymentMethodAsync [Theory, BitAutoData] public async Task UpdatePaymentMethod_Ok( Provider provider, TokenizedPaymentMethodRequestBody requestBody, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); await sutProvider.Sut.UpdatePaymentMethodAsync(provider.Id, requestBody); await sutProvider.GetDependency().Received(1).UpdatePaymentMethod( provider, Arg.Is( options => options.Type == requestBody.Type && options.Token == requestBody.Token)); await sutProvider.GetDependency().Received(1).SubscriptionUpdateAsync( provider.GatewaySubscriptionId, Arg.Is( options => options.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically)); } #endregion #region UpdateTaxInformationAsync [Theory, BitAutoData] public async Task UpdateTaxInformation_Ok( Provider provider, TaxInformationRequestBody requestBody, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); await sutProvider.Sut.UpdateTaxInformationAsync(provider.Id, requestBody); await sutProvider.GetDependency().Received(1).UpdateTaxInformation( provider, Arg.Is( options => options.Country == requestBody.Country && options.PostalCode == requestBody.PostalCode && options.TaxId == requestBody.TaxId && options.Line1 == requestBody.Line1 && options.Line2 == requestBody.Line2 && options.City == requestBody.City && options.State == requestBody.State)); } #endregion #region VerifyBankAccount [Theory, BitAutoData] public async Task VerifyBankAccount_Ok( Provider provider, VerifyBankAccountRequestBody requestBody, SutProvider sutProvider) { ConfigureStableInputs(provider, sutProvider); var result = await sutProvider.Sut.VerifyBankAccountAsync(provider.Id, requestBody); Assert.IsType(result); await sutProvider.GetDependency().Received(1).VerifyBankAccount( provider, (requestBody.Amount1, requestBody.Amount2)); } #endregion private static void ConfigureStableInputs( Provider provider, SutProvider sutProvider) { sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) .Returns(true); provider.Type = ProviderType.Msp; provider.Status = ProviderStatusType.Billable; sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency().ProviderProviderAdmin(provider.Id) .Returns(true); } }