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.Repositories; using Bit.Core.Test.Billing.Mocks; 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 Finalize - Trial Settings [Theory, BitAutoData] public async Task NoPaymentMethodAndTrialPeriod_SetsMissingPaymentMethodCancelBehavior( Organization organization, SutProvider sutProvider) { // Arrange var plan = MockPlans.Get(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() .CreateSubscriptionAsync(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) .CreateSubscriptionAsync(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 = MockPlans.Get(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() .CreateSubscriptionAsync(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) .CreateSubscriptionAsync(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 = MockPlans.Get(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() .CreateSubscriptionAsync(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) .CreateSubscriptionAsync(Arg.Any()); Assert.NotNull(capturedOptions); Assert.Equal(7, capturedOptions.TrialPeriodDays); Assert.Null(capturedOptions.TrialSettings); } #endregion [Theory, BitAutoData] public async Task UpdateOrganizationNameAndEmail_UpdatesStripeCustomer( Organization organization, SutProvider sutProvider) { organization.Name = "Short name"; CustomerUpdateOptions capturedOptions = null; sutProvider.GetDependency() .UpdateCustomerAsync( Arg.Is(id => id == organization.GatewayCustomerId), Arg.Do(options => capturedOptions = options)) .Returns(new Customer()); // Act await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization); // Assert await sutProvider.GetDependency() .Received(1) .UpdateCustomerAsync( organization.GatewayCustomerId, Arg.Any()); Assert.NotNull(capturedOptions); Assert.Equal(organization.BillingEmail, capturedOptions.Email); Assert.Equal(organization.DisplayName(), capturedOptions.Description); Assert.NotNull(capturedOptions.InvoiceSettings); Assert.NotNull(capturedOptions.InvoiceSettings.CustomFields); Assert.Single(capturedOptions.InvoiceSettings.CustomFields); var customField = capturedOptions.InvoiceSettings.CustomFields.First(); Assert.Equal(organization.SubscriberType(), customField.Name); Assert.Equal(organization.DisplayName(), customField.Value); } [Theory, BitAutoData] public async Task UpdateOrganizationNameAndEmail_WhenNameIsLong_UsesFullName( Organization organization, SutProvider sutProvider) { // Arrange var longName = "This is a very long organization name that exceeds thirty characters"; organization.Name = longName; CustomerUpdateOptions capturedOptions = null; sutProvider.GetDependency() .UpdateCustomerAsync( Arg.Is(id => id == organization.GatewayCustomerId), Arg.Do(options => capturedOptions = options)) .Returns(new Customer()); // Act await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization); // Assert await sutProvider.GetDependency() .Received(1) .UpdateCustomerAsync( organization.GatewayCustomerId, Arg.Any()); Assert.NotNull(capturedOptions); Assert.NotNull(capturedOptions.InvoiceSettings); Assert.NotNull(capturedOptions.InvoiceSettings.CustomFields); var customField = capturedOptions.InvoiceSettings.CustomFields.First(); Assert.Equal(longName, customField.Value); } [Theory, BitAutoData] public async Task UpdateOrganizationNameAndEmail_WhenGatewayCustomerIdIsNull_LogsWarningAndReturns( Organization organization, SutProvider sutProvider) { // Arrange organization.GatewayCustomerId = null; organization.Name = "Test Organization"; organization.BillingEmail = "billing@example.com"; var stripeAdapter = sutProvider.GetDependency(); // Act await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization); // Assert await stripeAdapter.DidNotReceive().UpdateCustomerAsync( Arg.Any(), Arg.Any()); } [Theory, BitAutoData] public async Task UpdateOrganizationNameAndEmail_WhenGatewayCustomerIdIsEmpty_LogsWarningAndReturns( Organization organization, SutProvider sutProvider) { // Arrange organization.GatewayCustomerId = ""; organization.Name = "Test Organization"; var stripeAdapter = sutProvider.GetDependency(); // Act await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization); // Assert await stripeAdapter.DidNotReceive().UpdateCustomerAsync( Arg.Any(), Arg.Any()); } [Theory, BitAutoData] public async Task UpdateOrganizationNameAndEmail_WhenNameIsNull_LogsWarningAndReturns( Organization organization, SutProvider sutProvider) { // Arrange organization.Name = null; organization.GatewayCustomerId = "cus_test123"; var stripeAdapter = sutProvider.GetDependency(); // Act await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization); // Assert await stripeAdapter.DidNotReceive().UpdateCustomerAsync( Arg.Any(), Arg.Any()); } [Theory, BitAutoData] public async Task UpdateOrganizationNameAndEmail_WhenNameIsEmpty_LogsWarningAndReturns( Organization organization, SutProvider sutProvider) { // Arrange organization.Name = ""; organization.GatewayCustomerId = "cus_test123"; var stripeAdapter = sutProvider.GetDependency(); // Act await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization); // Assert await stripeAdapter.DidNotReceive().UpdateCustomerAsync( Arg.Any(), Arg.Any()); } [Theory, BitAutoData] public async Task UpdateOrganizationNameAndEmail_WhenBillingEmailIsNull_UpdatesWithNull( Organization organization, SutProvider sutProvider) { // Arrange organization.Name = "Test Organization"; organization.BillingEmail = null; organization.GatewayCustomerId = "cus_test123"; var stripeAdapter = sutProvider.GetDependency(); // Act await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization); // Assert await stripeAdapter.Received(1).UpdateCustomerAsync( organization.GatewayCustomerId, Arg.Is(options => options.Email == null && options.Description == organization.Name)); } }