using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Organizations.Services; using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Stripe; using Xunit; namespace Bit.Core.Test.Billing.Services; [SutProviderCustomize] public class OrganizationBillingServiceTests { #region GetMetadata [Theory, BitAutoData] public async Task GetMetadata_Succeeds( Guid organizationId, Organization organization, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(organization); sutProvider.GetDependency().ListPlans().Returns(StaticStore.Plans.ToList()); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) .Returns(StaticStore.GetPlan(organization.PlanType)); var subscriberService = sutProvider.GetDependency(); var organizationSeatCount = new OrganizationSeatCounts { Users = 1, Sponsored = 0 }; var customer = new Customer { Discount = new Discount { Coupon = new Coupon { Id = StripeConstants.CouponIDs.SecretsManagerStandalone, AppliesTo = new CouponAppliesTo { Products = ["product_id"] } } } }; subscriberService .GetCustomer(organization, Arg.Is(options => options.Expand.Contains("discount.coupon.applies_to"))) .Returns(customer); subscriberService.GetSubscription(organization).Returns(new Subscription { Items = new StripeList { Data = [ new SubscriptionItem { Plan = new Plan { ProductId = "product_id" } } ] } }); sutProvider.GetDependency() .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) .Returns(new OrganizationSeatCounts { Users = 1, Sponsored = 0 }); var metadata = await sutProvider.Sut.GetMetadata(organizationId); Assert.True(metadata!.IsOnSecretsManagerStandalone); } #endregion #region GetMetadata - Null Customer or Subscription [Theory, BitAutoData] public async Task GetMetadata_WhenCustomerOrSubscriptionIsNull_ReturnsDefaultMetadata( Guid organizationId, Organization organization, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(organization); sutProvider.GetDependency().ListPlans().Returns(StaticStore.Plans.ToList()); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) .Returns(StaticStore.GetPlan(organization.PlanType)); sutProvider.GetDependency() .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) .Returns(new OrganizationSeatCounts { Users = 1, Sponsored = 0 }); var subscriberService = sutProvider.GetDependency(); // Set up subscriber service to return null for customer subscriberService .GetCustomer(organization, Arg.Is(options => options.Expand.FirstOrDefault() == "discount.coupon.applies_to")) .Returns((Customer)null); // Set up subscriber service to return null for subscription subscriberService.GetSubscription(organization).Returns((Subscription)null); var metadata = await sutProvider.Sut.GetMetadata(organizationId); Assert.NotNull(metadata); Assert.False(metadata!.IsOnSecretsManagerStandalone); Assert.Equal(1, metadata.OrganizationOccupiedSeats); } #endregion #region Finalize - Trial Settings [Theory, BitAutoData] public async Task NoPaymentMethodAndTrialPeriod_SetsMissingPaymentMethodCancelBehavior( Organization organization, SutProvider sutProvider) { // Arrange var plan = StaticStore.GetPlan(PlanType.TeamsAnnually); organization.PlanType = PlanType.TeamsAnnually; organization.GatewayCustomerId = "cus_test123"; organization.GatewaySubscriptionId = null; var subscriptionSetup = new SubscriptionSetup { PlanType = PlanType.TeamsAnnually, PasswordManagerOptions = new SubscriptionSetup.PasswordManager { Seats = 5, Storage = null, PremiumAccess = false }, SecretsManagerOptions = null, SkipTrial = false }; var sale = new OrganizationSale { Organization = organization, SubscriptionSetup = subscriptionSetup }; sutProvider.GetDependency() .GetPlanOrThrow(PlanType.TeamsAnnually) .Returns(plan); sutProvider.GetDependency() .Run(organization) .Returns(false); var customer = new Customer { Id = "cus_test123", Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } }; sutProvider.GetDependency() .GetCustomerOrThrow(organization, Arg.Any()) .Returns(customer); SubscriptionCreateOptions capturedOptions = null; sutProvider.GetDependency() .SubscriptionCreateAsync(Arg.Do(options => capturedOptions = options)) .Returns(new Subscription { Id = "sub_test123", Status = StripeConstants.SubscriptionStatus.Trialing }); sutProvider.GetDependency() .ReplaceAsync(organization) .Returns(Task.CompletedTask); // Act await sutProvider.Sut.Finalize(sale); // Assert await sutProvider.GetDependency() .Received(1) .SubscriptionCreateAsync(Arg.Any()); Assert.NotNull(capturedOptions); Assert.Equal(7, capturedOptions.TrialPeriodDays); Assert.NotNull(capturedOptions.TrialSettings); Assert.NotNull(capturedOptions.TrialSettings.EndBehavior); Assert.Equal("cancel", capturedOptions.TrialSettings.EndBehavior.MissingPaymentMethod); } [Theory, BitAutoData] public async Task NoPaymentMethodButNoTrial_DoesNotSetMissingPaymentMethodBehavior( Organization organization, SutProvider sutProvider) { // Arrange var plan = StaticStore.GetPlan(PlanType.TeamsAnnually); organization.PlanType = PlanType.TeamsAnnually; organization.GatewayCustomerId = "cus_test123"; organization.GatewaySubscriptionId = null; var subscriptionSetup = new SubscriptionSetup { PlanType = PlanType.TeamsAnnually, PasswordManagerOptions = new SubscriptionSetup.PasswordManager { Seats = 5, Storage = null, PremiumAccess = false }, SecretsManagerOptions = null, SkipTrial = true // This will result in TrialPeriodDays = 0 }; var sale = new OrganizationSale { Organization = organization, SubscriptionSetup = subscriptionSetup }; sutProvider.GetDependency() .GetPlanOrThrow(PlanType.TeamsAnnually) .Returns(plan); sutProvider.GetDependency() .Run(organization) .Returns(false); var customer = new Customer { Id = "cus_test123", Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } }; sutProvider.GetDependency() .GetCustomerOrThrow(organization, Arg.Any()) .Returns(customer); SubscriptionCreateOptions capturedOptions = null; sutProvider.GetDependency() .SubscriptionCreateAsync(Arg.Do(options => capturedOptions = options)) .Returns(new Subscription { Id = "sub_test123", Status = StripeConstants.SubscriptionStatus.Active }); sutProvider.GetDependency() .ReplaceAsync(organization) .Returns(Task.CompletedTask); // Act await sutProvider.Sut.Finalize(sale); // Assert await sutProvider.GetDependency() .Received(1) .SubscriptionCreateAsync(Arg.Any()); Assert.NotNull(capturedOptions); Assert.Equal(0, capturedOptions.TrialPeriodDays); Assert.Null(capturedOptions.TrialSettings); } [Theory, BitAutoData] public async Task HasPaymentMethodAndTrialPeriod_DoesNotSetMissingPaymentMethodBehavior( Organization organization, SutProvider sutProvider) { // Arrange var plan = StaticStore.GetPlan(PlanType.TeamsAnnually); organization.PlanType = PlanType.TeamsAnnually; organization.GatewayCustomerId = "cus_test123"; organization.GatewaySubscriptionId = null; var subscriptionSetup = new SubscriptionSetup { PlanType = PlanType.TeamsAnnually, PasswordManagerOptions = new SubscriptionSetup.PasswordManager { Seats = 5, Storage = null, PremiumAccess = false }, SecretsManagerOptions = null, SkipTrial = false }; var sale = new OrganizationSale { Organization = organization, SubscriptionSetup = subscriptionSetup }; sutProvider.GetDependency() .GetPlanOrThrow(PlanType.TeamsAnnually) .Returns(plan); sutProvider.GetDependency() .Run(organization) .Returns(true); // Has payment method var customer = new Customer { Id = "cus_test123", Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } }; sutProvider.GetDependency() .GetCustomerOrThrow(organization, Arg.Any()) .Returns(customer); SubscriptionCreateOptions capturedOptions = null; sutProvider.GetDependency() .SubscriptionCreateAsync(Arg.Do(options => capturedOptions = options)) .Returns(new Subscription { Id = "sub_test123", Status = StripeConstants.SubscriptionStatus.Trialing }); sutProvider.GetDependency() .ReplaceAsync(organization) .Returns(Task.CompletedTask); // Act await sutProvider.Sut.Finalize(sale); // Assert await sutProvider.GetDependency() .Received(1) .SubscriptionCreateAsync(Arg.Any()); Assert.NotNull(capturedOptions); Assert.Equal(7, capturedOptions.TrialPeriodDays); Assert.Null(capturedOptions.TrialSettings); } #endregion }