diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index b9db8d81f9..4915e5ef8e 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -3,10 +3,10 @@ using System.Diagnostics; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; -using Bit.Api.Billing.Queries.Organizations; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Billing.Organizations.Services; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Services; @@ -28,7 +28,7 @@ public class OrganizationBillingController( ICurrentContext currentContext, IOrganizationBillingService organizationBillingService, IOrganizationRepository organizationRepository, - IOrganizationWarningsQuery organizationWarningsQuery, + IGetOrganizationWarningsQuery getOrganizationWarningsQuery, IPaymentService paymentService, IPricingClient pricingClient, ISubscriberService subscriberService, @@ -363,7 +363,7 @@ public class OrganizationBillingController( public async Task GetWarningsAsync([FromRoute] Guid organizationId) { /* - * We'll keep these available at the User level, because we're hiding any pertinent information and + * We'll keep these available at the User level because we're hiding any pertinent information, and * we want to throw as few errors as possible since these are not core features. */ if (!await currentContext.OrganizationUser(organizationId)) @@ -378,9 +378,9 @@ public class OrganizationBillingController( return Error.NotFound(); } - var response = await organizationWarningsQuery.Run(organization); + var warnings = await getOrganizationWarningsQuery.Run(organization); - return TypedResults.Ok(response); + return TypedResults.Ok(warnings); } diff --git a/src/Api/Billing/Registrations.cs b/src/Api/Billing/Registrations.cs deleted file mode 100644 index cb92098333..0000000000 --- a/src/Api/Billing/Registrations.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bit.Api.Billing.Queries.Organizations; - -namespace Bit.Api.Billing; - -public static class Registrations -{ - public static void AddBillingQueries(this IServiceCollection services) - { - services.AddTransient(); - } -} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index c2a75c9278..699fa3f804 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -27,7 +27,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Bit.Api.Auth.Models.Request.WebAuthn; -using Bit.Api.Billing; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Tools.ImportFeatures; @@ -184,7 +183,6 @@ public class Startup services.AddImportServices(); services.AddPhishingDomainServices(globalSettings); - services.AddBillingQueries(); services.AddSendServices(); // Authorization Handlers diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 3fb5526254..39ee3ec1ec 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -33,6 +33,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddPaymentOperations(); services.AddOrganizationLicenseCommandsQueries(); + services.AddTransient(); } private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services) diff --git a/src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs b/src/Core/Billing/Organizations/Models/OrganizationWarnings.cs similarity index 90% rename from src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs rename to src/Core/Billing/Organizations/Models/OrganizationWarnings.cs index e124bdc318..4507c84083 100644 --- a/src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationWarnings.cs @@ -1,7 +1,6 @@ -#nullable enable -namespace Bit.Api.Billing.Models.Responses.Organizations; +namespace Bit.Core.Billing.Organizations.Models; -public record OrganizationWarningsResponse +public record OrganizationWarnings { public FreeTrialWarning? FreeTrial { get; set; } public InactiveSubscriptionWarning? InactiveSubscription { get; set; } diff --git a/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs similarity index 51% rename from src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs rename to src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs index 7fbdf3c2b0..a46d7483e7 100644 --- a/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs +++ b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs @@ -1,42 +1,44 @@ // ReSharper disable InconsistentNaming -#nullable enable - -using Bit.Api.Billing.Models.Responses.Organizations; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Services; using Stripe; -using static Bit.Core.Billing.Utilities; -using FreeTrialWarning = Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.FreeTrialWarning; +using FreeTrialWarning = Bit.Core.Billing.Organizations.Models.OrganizationWarnings.FreeTrialWarning; using InactiveSubscriptionWarning = - Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.InactiveSubscriptionWarning; + Bit.Core.Billing.Organizations.Models.OrganizationWarnings.InactiveSubscriptionWarning; using ResellerRenewalWarning = - Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.ResellerRenewalWarning; + Bit.Core.Billing.Organizations.Models.OrganizationWarnings.ResellerRenewalWarning; -namespace Bit.Api.Billing.Queries.Organizations; +namespace Bit.Core.Billing.Organizations.Queries; -public interface IOrganizationWarningsQuery +using static StripeConstants; + +public interface IGetOrganizationWarningsQuery { - Task Run( + Task Run( Organization organization); } -public class OrganizationWarningsQuery( +public class GetOrganizationWarningsQuery( ICurrentContext currentContext, IProviderRepository providerRepository, + ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, - ISubscriberService subscriberService) : IOrganizationWarningsQuery + ISubscriberService subscriberService) : IGetOrganizationWarningsQuery { - public async Task Run( + public async Task Run( Organization organization) { - var response = new OrganizationWarningsResponse(); + var response = new OrganizationWarnings(); var subscription = await subscriberService.GetSubscription(organization, @@ -69,7 +71,7 @@ public class OrganizationWarningsQuery( if (subscription is not { - Status: StripeConstants.SubscriptionStatus.Trialing, + Status: SubscriptionStatus.Trialing, TrialEnd: not null, Customer: not null }) @@ -79,10 +81,13 @@ public class OrganizationWarningsQuery( var customer = subscription.Customer; + var hasUnverifiedBankAccount = await HasUnverifiedBankAccount(organization); + var hasPaymentMethod = !string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) || !string.IsNullOrEmpty(customer.DefaultSourceId) || - customer.Metadata.ContainsKey(StripeConstants.MetadataKeys.BraintreeCustomerId); + hasUnverifiedBankAccount || + customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId); if (hasPaymentMethod) { @@ -101,49 +106,58 @@ public class OrganizationWarningsQuery( Provider? provider, Subscription subscription) { - if (organization.Enabled && subscription.Status is StripeConstants.SubscriptionStatus.Trialing) - { - var isStripeCustomerWithoutPayment = - subscription.Customer.InvoiceSettings.DefaultPaymentMethodId is null; - var isBraintreeCustomer = - subscription.Customer.Metadata.ContainsKey(BraintreeCustomerIdKey); - var hasNoPaymentMethod = isStripeCustomerWithoutPayment && !isBraintreeCustomer; + var isOrganizationOwner = await currentContext.OrganizationOwner(organization.Id); - if (hasNoPaymentMethod && await currentContext.OrganizationOwner(organization.Id)) - { - return new InactiveSubscriptionWarning { Resolution = "add_payment_method_optional_trial" }; - } - } - - if (organization.Enabled || - subscription.Status is not StripeConstants.SubscriptionStatus.Unpaid - and not StripeConstants.SubscriptionStatus.Canceled) + switch (organization.Enabled) { - return null; - } - - if (provider != null) - { - return new InactiveSubscriptionWarning { Resolution = "contact_provider" }; - } - - if (await currentContext.OrganizationOwner(organization.Id)) - { - return subscription.Status switch - { - StripeConstants.SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning + // Member of an enabled, trialing organization. + case true when subscription.Status is SubscriptionStatus.Trialing: { - Resolution = "add_payment_method" - }, - StripeConstants.SubscriptionStatus.Canceled => new InactiveSubscriptionWarning - { - Resolution = "resubscribe" - }, - _ => null - }; - } + var hasUnverifiedBankAccount = await HasUnverifiedBankAccount(organization); - return new InactiveSubscriptionWarning { Resolution = "contact_owner" }; + var hasPaymentMethod = + !string.IsNullOrEmpty(subscription.Customer.InvoiceSettings.DefaultPaymentMethodId) || + !string.IsNullOrEmpty(subscription.Customer.DefaultSourceId) || + hasUnverifiedBankAccount || + subscription.Customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId); + + // If this member is the owner and there's no payment method on file, ask them to add one. + return isOrganizationOwner && !hasPaymentMethod + ? new InactiveSubscriptionWarning { Resolution = "add_payment_method_optional_trial" } + : null; + } + // Member of disabled and unpaid or canceled organization. + case false when subscription.Status is SubscriptionStatus.Unpaid or SubscriptionStatus.Canceled: + { + // If the organization is managed by a provider, return a warning asking them to contact the provider. + if (provider != null) + { + return new InactiveSubscriptionWarning { Resolution = "contact_provider" }; + } + + /* If the organization is not managed by a provider and this user is the owner, return an action warning based + on the subscription status. */ + if (isOrganizationOwner) + { + return subscription.Status switch + { + SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning + { + Resolution = "add_payment_method" + }, + SubscriptionStatus.Canceled => new InactiveSubscriptionWarning + { + Resolution = "resubscribe" + }, + _ => null + }; + } + + // Otherwise, this member is not the owner, and we need to ask them to contact the owner. + return new InactiveSubscriptionWarning { Resolution = "contact_owner" }; + } + default: return null; + } } private async Task GetResellerRenewalWarning( @@ -158,7 +172,7 @@ public class OrganizationWarningsQuery( return null; } - if (subscription.CollectionMethod != StripeConstants.CollectionMethod.SendInvoice) + if (subscription.CollectionMethod != CollectionMethod.SendInvoice) { return null; } @@ -168,8 +182,8 @@ public class OrganizationWarningsQuery( // ReSharper disable once ConvertIfStatementToSwitchStatement if (subscription is { - Status: StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active, - LatestInvoice: null or { Status: StripeConstants.InvoiceStatus.Paid } + Status: SubscriptionStatus.Trialing or SubscriptionStatus.Active, + LatestInvoice: null or { Status: InvoiceStatus.Paid } } && (subscription.CurrentPeriodEnd - now).TotalDays <= 14) { return new ResellerRenewalWarning @@ -184,8 +198,8 @@ public class OrganizationWarningsQuery( if (subscription is { - Status: StripeConstants.SubscriptionStatus.Active, - LatestInvoice: { Status: StripeConstants.InvoiceStatus.Open, DueDate: not null } + Status: SubscriptionStatus.Active, + LatestInvoice: { Status: InvoiceStatus.Open, DueDate: not null } } && subscription.LatestInvoice.DueDate > now) { return new ResellerRenewalWarning @@ -200,7 +214,7 @@ public class OrganizationWarningsQuery( } // ReSharper disable once InvertIf - if (subscription.Status == StripeConstants.SubscriptionStatus.PastDue) + if (subscription.Status == SubscriptionStatus.PastDue) { var openInvoices = await stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions { @@ -226,4 +240,22 @@ public class OrganizationWarningsQuery( return null; } + + private async Task HasUnverifiedBankAccount( + Organization organization) + { + var setupIntentId = await setupIntentCache.Get(organization.Id); + + if (string.IsNullOrEmpty(setupIntentId)) + { + return false; + } + + var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions + { + Expand = ["payment_method"] + }); + + return setupIntent.IsUnverifiedBankAccount(); + } } diff --git a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs similarity index 98% rename from test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs rename to test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs index 3b69dc2dfc..8570dfc6be 100644 --- a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs @@ -14,7 +14,7 @@ using NSubstitute; using Xunit; using JsonSerializer = System.Text.Json.JsonSerializer; -namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Test.Billing.Organizations.Commands; [SutProviderCustomize] public class UpdateOrganizationLicenseCommandTests diff --git a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs similarity index 98% rename from test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs rename to test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs index a81b390ae1..ed3698fb1d 100644 --- a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs @@ -18,7 +18,7 @@ using NSubstitute.ReturnsExtensions; using Stripe; using Xunit; -namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Test.Billing.Organizations.Queries; [SubscriptionInfoCustomize] [OrganizationLicenseCustomize] diff --git a/test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs similarity index 70% rename from test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs rename to test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs index 67979f506e..54c982192b 100644 --- a/test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs @@ -1,12 +1,13 @@ -using Bit.Api.Billing.Queries.Organizations; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Services; -using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -15,17 +16,17 @@ using Stripe; using Stripe.TestHelpers; using Xunit; -namespace Bit.Api.Test.Billing.Queries.Organizations; +namespace Bit.Core.Test.Billing.Organizations.Queries; [SutProviderCustomize] -public class OrganizationWarningsQueryTests +public class GetOrganizationWarningsQueryTests { private static readonly string[] _requiredExpansions = ["customer", "latest_invoice", "test_clock"]; [Theory, BitAutoData] public async Task Run_NoSubscription_NoWarnings( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { sutProvider.GetDependency() .GetSubscription(organization, Arg.Is(options => @@ -46,7 +47,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_FreeTrialWarning( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { var now = DateTime.UtcNow; @@ -70,6 +71,7 @@ public class OrganizationWarningsQueryTests }); sutProvider.GetDependency().EditSubscription(organization.Id).Returns(true); + sutProvider.GetDependency().Get(organization.Id).Returns((string?)null); var response = await sutProvider.Sut.Run(organization); @@ -79,10 +81,90 @@ public class OrganizationWarningsQueryTests }); } + [Theory, BitAutoData] + public async Task Run_Has_FreeTrialWarning_WithUnverifiedBankAccount_NoWarning( + Organization organization, + SutProvider sutProvider) + { + var now = DateTime.UtcNow; + const string setupIntentId = "setup_intent_id"; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Trialing, + TrialEnd = now.AddDays(7), + Customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }, + TestClock = new TestClock + { + FrozenTime = now + } + }); + + sutProvider.GetDependency().EditSubscription(organization.Id).Returns(true); + sutProvider.GetDependency().Get(organization.Id).Returns(setupIntentId); + sutProvider.GetDependency().SetupIntentGet(setupIntentId, Arg.Is( + options => options.Expand.Contains("payment_method"))).Returns(new SetupIntent + { + Status = "requires_action", + NextAction = new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, + PaymentMethod = new PaymentMethod + { + UsBankAccount = new PaymentMethodUsBankAccount() + } + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.Null(response.FreeTrial); + } + + [Theory, BitAutoData] + public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethodOptionalTrial( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Trialing, + Customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }); + + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); + sutProvider.GetDependency().Get(organization.Id).Returns((string?)null); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + InactiveSubscription.Resolution: "add_payment_method_optional_trial" + }); + } + [Theory, BitAutoData] public async Task Run_Has_InactiveSubscriptionWarning_ContactProvider( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { organization.Enabled = false; @@ -109,7 +191,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethod( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { organization.Enabled = false; @@ -135,7 +217,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_InactiveSubscriptionWarning_Resubscribe( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { organization.Enabled = false; @@ -161,7 +243,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_InactiveSubscriptionWarning_ContactOwner( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { organization.Enabled = false; @@ -187,7 +269,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_ResellerRenewalWarning_Upcoming( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { var now = DateTime.UtcNow; @@ -225,7 +307,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_ResellerRenewalWarning_Issued( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { var now = DateTime.UtcNow; @@ -269,7 +351,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_ResellerRenewalWarning_PastDue( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { var now = DateTime.UtcNow; diff --git a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQueryTests.cs similarity index 98% rename from test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs rename to test/Core.Test/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQueryTests.cs index 5d3d4acff7..f0fa3db84e 100644 --- a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQueryTests.cs @@ -12,7 +12,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; using Xunit; -namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Test.Billing.Organizations.Queries; [SutProviderCustomize] public class GetSelfHostedOrganizationLicenseQueryTests