From 982d1bc55883714e96c7e2ce28d406dd162ce80b Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 6 Nov 2024 09:44:16 +0100 Subject: [PATCH] [PM-13470] Allow creating clients for Multi-organization enterprise (#4977) --- .../AdminConsole/Services/ProviderService.cs | 30 ++++++++++++++----- .../Billing/ProviderBillingService.cs | 11 ++----- .../CreateClientOrganizationRequestBody.cs | 2 +- .../Responses/ProviderSubscriptionResponse.cs | 5 ++++ .../Billing/Extensions/BillingExtensions.cs | 2 +- 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index b6773f0bd..48ea903ad 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -392,7 +392,9 @@ public class ProviderService : IProviderService var organization = await _organizationRepository.GetByIdAsync(organizationId); - ThrowOnInvalidPlanType(organization.PlanType); + var provider = await _providerRepository.GetByIdAsync(providerId); + + ThrowOnInvalidPlanType(provider.Type, organization.PlanType); if (organization.UseSecretsManager) { @@ -407,8 +409,6 @@ public class ProviderService : IProviderService Key = key, }; - var provider = await _providerRepository.GetByIdAsync(providerId); - await ApplyProviderPriceRateAsync(organization, provider); await _providerOrganizationRepository.CreateAsync(providerOrganization); @@ -547,7 +547,7 @@ public class ProviderService : IProviderService var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && provider.IsBillable(); - ThrowOnInvalidPlanType(organizationSignup.Plan, consolidatedBillingEnabled); + ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan, consolidatedBillingEnabled); var (organization, _, defaultCollection) = consolidatedBillingEnabled ? await _organizationService.SignupClientAsync(organizationSignup) @@ -687,11 +687,27 @@ public class ProviderService : IProviderService return confirmedOwnersIds.Except(providerUserIds).Any(); } - private void ThrowOnInvalidPlanType(PlanType requestedType, bool consolidatedBillingEnabled = false) + private void ThrowOnInvalidPlanType(ProviderType providerType, PlanType requestedType, bool consolidatedBillingEnabled = false) { - if (consolidatedBillingEnabled && requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly)) + if (consolidatedBillingEnabled) { - throw new BadRequestException($"Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed."); + switch (providerType) + { + case ProviderType.Msp: + if (requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly)) + { + throw new BadRequestException($"Managed Service Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed."); + } + break; + case ProviderType.MultiOrganizationEnterprise: + if (requestedType is not (PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually)) + { + throw new BadRequestException($"Multi-organization Enterprise Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed."); + } + break; + default: + throw new BadRequestException($"Unsupported provider type {providerType}."); + } } if (ProviderDisallowedOrganizationTypes.Contains(requestedType)) diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 32698eaaf..e02160b06 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -209,16 +209,9 @@ public class ProviderBillingService( { ArgumentNullException.ThrowIfNull(provider); - if (provider.Type != ProviderType.Msp) + if (!provider.SupportsConsolidatedBilling()) { - logger.LogError("Non-MSP provider ({ProviderID}) cannot scale their seats", provider.Id); - - throw new BillingException(); - } - - if (!planType.SupportsConsolidatedBilling()) - { - logger.LogError("Cannot scale provider ({ProviderID}) seats for plan type {PlanType} as it does not support consolidated billing", provider.Id, planType.ToString()); + logger.LogError("Provider ({ProviderID}) cannot scale their seats", provider.Id); throw new BillingException(); } diff --git a/src/Api/Billing/Models/Requests/CreateClientOrganizationRequestBody.cs b/src/Api/Billing/Models/Requests/CreateClientOrganizationRequestBody.cs index 39b2e3323..95836151d 100644 --- a/src/Api/Billing/Models/Requests/CreateClientOrganizationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/CreateClientOrganizationRequestBody.cs @@ -12,7 +12,7 @@ public class CreateClientOrganizationRequestBody [Required(ErrorMessage = "'ownerEmail' must be provided")] public string OwnerEmail { get; set; } - [EnumMatches(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly, ErrorMessage = "'planType' must be Teams (Monthly) or Enterprise (Monthly)")] + [EnumMatches(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly, PlanType.EnterpriseAnnually, ErrorMessage = "'planType' must be Teams (Monthly), Enterprise (Monthly) or Enterprise (Annually)")] public PlanType PlanType { get; set; } [Range(1, int.MaxValue, ErrorMessage = "'seats' must be greater than 0")] diff --git a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs index e9902f98b..79e2dc0e0 100644 --- a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs +++ b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; using Bit.Core.Utilities; using Stripe; @@ -35,6 +36,8 @@ public record ProviderSubscriptionResponse( var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence; return new ProviderPlanResponse( plan.Name, + plan.Type, + plan.ProductTier, configuredProviderPlan.SeatMinimum, configuredProviderPlan.PurchasedSeats, configuredProviderPlan.AssignedSeats, @@ -59,6 +62,8 @@ public record ProviderSubscriptionResponse( public record ProviderPlanResponse( string PlanName, + PlanType Type, + ProductTierType ProductTier, int SeatMinimum, int PurchasedSeats, int AssignedSeats, diff --git a/src/Core/Billing/Extensions/BillingExtensions.cs b/src/Core/Billing/Extensions/BillingExtensions.cs index 21974b318..02e8de924 100644 --- a/src/Core/Billing/Extensions/BillingExtensions.cs +++ b/src/Core/Billing/Extensions/BillingExtensions.cs @@ -43,5 +43,5 @@ public static class BillingExtensions }; public static bool SupportsConsolidatedBilling(this PlanType planType) - => planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly; + => planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually; }