1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-12 00:28:11 -05:00

[PM-16684] Integrate Pricing Service behind FF (#5276)

* Remove gRPC and convert PricingClient to HttpClient wrapper

* Add PlanType.GetProductTier extension

Many instances of StaticStore use are just to get the ProductTierType of a PlanType, but this can be derived from the PlanType itself without having to fetch the entire plan.

* Remove invocations of the StaticStore in non-Test code

* Deprecate StaticStore entry points

* Run dotnet format

* Matt's feedback

* Run dotnet format

* Rui's feedback

* Run dotnet format

* Replacements since approval

* Run dotnet format
This commit is contained in:
Alex Morask 2025-02-27 07:55:46 -05:00 committed by GitHub
parent bd66f06bd9
commit a2e665cb96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 1178 additions and 712 deletions

View File

@ -5,12 +5,12 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Stripe; using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Providers; namespace Bit.Commercial.Core.AdminConsole.Providers;
@ -27,6 +27,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly ISubscriberService _subscriberService; private readonly ISubscriberService _subscriberService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
public RemoveOrganizationFromProviderCommand( public RemoveOrganizationFromProviderCommand(
IEventService eventService, IEventService eventService,
@ -38,7 +39,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
IFeatureService featureService, IFeatureService featureService,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient)
{ {
_eventService = eventService; _eventService = eventService;
_mailService = mailService; _mailService = mailService;
@ -50,6 +52,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_subscriberService = subscriberService; _subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
} }
public async Task RemoveOrganizationFromProvider( public async Task RemoveOrganizationFromProvider(
@ -110,7 +113,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
Email = organization.BillingEmail Email = organization.BillingEmail
}); });
var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager; var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var subscriptionCreateOptions = new SubscriptionCreateOptions var subscriptionCreateOptions = new SubscriptionCreateOptions
{ {
@ -124,7 +127,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
}, },
OffSession = true, OffSession = true,
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
Items = [new SubscriptionItemOptions { Price = plan.StripeSeatPlanId, Quantity = organization.Seats }] Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
}; };
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);

View File

@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -50,6 +51,7 @@ public class ProviderService : IProviderService
private readonly IDataProtectorTokenFactory<ProviderDeleteTokenable> _providerDeleteTokenDataFactory; private readonly IDataProtectorTokenFactory<ProviderDeleteTokenable> _providerDeleteTokenDataFactory;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly IPricingClient _pricingClient;
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository, IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
@ -58,7 +60,7 @@ public class ProviderService : IProviderService
IOrganizationRepository organizationRepository, GlobalSettings globalSettings, IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService, ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory, IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService) IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient)
{ {
_providerRepository = providerRepository; _providerRepository = providerRepository;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
@ -77,6 +79,7 @@ public class ProviderService : IProviderService
_providerDeleteTokenDataFactory = providerDeleteTokenDataFactory; _providerDeleteTokenDataFactory = providerDeleteTokenDataFactory;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_pricingClient = pricingClient;
} }
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null) public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null)
@ -452,30 +455,31 @@ public class ProviderService : IProviderService
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{ {
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
GetStripeSeatPlanId(organization.PlanType));
var subscriptionItem = await GetSubscriptionItemAsync(
organization.GatewaySubscriptionId,
plan.PasswordManager.StripeSeatPlanId);
var extractedPlanType = PlanTypeMappings(organization); var extractedPlanType = PlanTypeMappings(organization);
var extractedPlan = await _pricingClient.GetPlanOrThrow(extractedPlanType);
if (subscriptionItem != null) if (subscriptionItem != null)
{ {
await UpdateSubscriptionAsync(subscriptionItem, GetStripeSeatPlanId(extractedPlanType), organization); await UpdateSubscriptionAsync(subscriptionItem, extractedPlan.PasswordManager.StripeSeatPlanId, organization);
} }
} }
await _organizationRepository.UpsertAsync(organization); await _organizationRepository.UpsertAsync(organization);
} }
private async Task<Stripe.SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId) private async Task<SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId)
{ {
var subscriptionDetails = await _stripeAdapter.SubscriptionGetAsync(subscriptionId); var subscriptionDetails = await _stripeAdapter.SubscriptionGetAsync(subscriptionId);
return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId); return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId);
} }
private static string GetStripeSeatPlanId(PlanType planType) private async Task UpdateSubscriptionAsync(SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization)
{
return StaticStore.GetPlan(planType).PasswordManager.StripeSeatPlanId;
}
private async Task UpdateSubscriptionAsync(Stripe.SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization)
{ {
try try
{ {

View File

@ -10,6 +10,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Services.Contracts;
@ -32,6 +33,7 @@ public class ProviderBillingService(
ILogger<ProviderBillingService> logger, ILogger<ProviderBillingService> logger,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IPaymentService paymentService, IPaymentService paymentService,
IPricingClient pricingClient,
IProviderInvoiceItemRepository providerInvoiceItemRepository, IProviderInvoiceItemRepository providerInvoiceItemRepository,
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IProviderPlanRepository providerPlanRepository, IProviderPlanRepository providerPlanRepository,
@ -77,8 +79,7 @@ public class ProviderBillingService(
var managedPlanType = await GetManagedPlanTypeAsync(provider, organization); var managedPlanType = await GetManagedPlanTypeAsync(provider, organization);
// TODO: Replace with PricingClient var plan = await pricingClient.GetPlanOrThrow(managedPlanType);
var plan = StaticStore.GetPlan(managedPlanType);
organization.Plan = plan.Name; organization.Plan = plan.Name;
organization.PlanType = plan.Type; organization.PlanType = plan.Type;
organization.MaxCollections = plan.PasswordManager.MaxCollections; organization.MaxCollections = plan.PasswordManager.MaxCollections;
@ -154,7 +155,8 @@ public class ProviderBillingService(
return; return;
} }
var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType); var oldPlanConfiguration = await pricingClient.GetPlanOrThrow(plan.PlanType);
var newPlanConfiguration = await pricingClient.GetPlanOrThrow(command.NewPlan);
plan.PlanType = command.NewPlan; plan.PlanType = command.NewPlan;
await providerPlanRepository.ReplaceAsync(plan); await providerPlanRepository.ReplaceAsync(plan);
@ -178,7 +180,7 @@ public class ProviderBillingService(
[ [
new SubscriptionItemOptions new SubscriptionItemOptions
{ {
Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId, Price = newPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId,
Quantity = oldSubscriptionItem!.Quantity Quantity = oldSubscriptionItem!.Quantity
}, },
new SubscriptionItemOptions new SubscriptionItemOptions
@ -204,7 +206,7 @@ public class ProviderBillingService(
throw new ConflictException($"Organization '{providerOrganization.Id}' not found."); throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
} }
organization.PlanType = command.NewPlan; organization.PlanType = command.NewPlan;
organization.Plan = StaticStore.GetPlan(command.NewPlan).Name; organization.Plan = newPlanConfiguration.Name;
await organizationRepository.ReplaceAsync(organization); await organizationRepository.ReplaceAsync(organization);
} }
} }
@ -347,7 +349,7 @@ public class ProviderBillingService(
{ {
var (organization, _) = pair; var (organization, _) = pair;
var planName = DerivePlanName(provider, organization); var planName = await DerivePlanName(provider, organization);
var addable = new AddableOrganization( var addable = new AddableOrganization(
organization.Id, organization.Id,
@ -368,7 +370,7 @@ public class ProviderBillingService(
return addable with { Disabled = requiresPurchase }; return addable with { Disabled = requiresPurchase };
})); }));
string DerivePlanName(Provider localProvider, Organization localOrganization) async Task<string> DerivePlanName(Provider localProvider, Organization localOrganization)
{ {
if (localProvider.Type == ProviderType.Msp) if (localProvider.Type == ProviderType.Msp)
{ {
@ -380,8 +382,7 @@ public class ProviderBillingService(
}; };
} }
// TODO: Replace with PricingClient var plan = await pricingClient.GetPlanOrThrow(localOrganization.PlanType);
var plan = StaticStore.GetPlan(localOrganization.PlanType);
return plan.Name; return plan.Name;
} }
} }
@ -568,7 +569,7 @@ public class ProviderBillingService(
foreach (var providerPlan in providerPlans) foreach (var providerPlan in providerPlans)
{ {
var plan = StaticStore.GetPlan(providerPlan.PlanType); var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
if (!providerPlan.IsConfigured()) if (!providerPlan.IsConfigured())
{ {
@ -652,8 +653,10 @@ public class ProviderBillingService(
if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum) if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum)
{ {
var priceId = StaticStore.GetPlan(newPlanConfiguration.Plan).PasswordManager var newPlan = await pricingClient.GetPlanOrThrow(newPlanConfiguration.Plan);
.StripeProviderPortalSeatPlanId;
var priceId = newPlan.PasswordManager.StripeProviderPortalSeatPlanId;
var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId); var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId);
if (providerPlan.PurchasedSeats == 0) if (providerPlan.PurchasedSeats == 0)
@ -717,7 +720,7 @@ public class ProviderBillingService(
ProviderPlan providerPlan, ProviderPlan providerPlan,
int newlyAssignedSeats) => async (currentlySubscribedSeats, newlySubscribedSeats) => int newlyAssignedSeats) => async (currentlySubscribedSeats, newlySubscribedSeats) =>
{ {
var plan = StaticStore.GetPlan(providerPlan.PlanType); var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
await paymentService.AdjustSeats( await paymentService.AdjustSeats(
provider, provider,
@ -741,7 +744,7 @@ public class ProviderBillingService(
var providerOrganizations = var providerOrganizations =
await providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id); await providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id);
var plan = StaticStore.GetPlan(planType); var plan = await pricingClient.GetPlanOrThrow(planType);
return providerOrganizations return providerOrganizations
.Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed) .Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed)

View File

@ -28,6 +28,7 @@ public class MaxProjectsQuery : IMaxProjectsQuery
throw new NotFoundException(); throw new NotFoundException();
} }
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122
var plan = StaticStore.GetPlan(org.PlanType); var plan = StaticStore.GetPlan(org.PlanType);
if (plan?.SecretsManager == null) if (plan?.SecretsManager == null)
{ {

View File

@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -205,6 +206,8 @@ public class RemoveOrganizationFromProviderCommandTests
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync( sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId, providerOrganization.OrganizationId,
[], [],

View File

@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -550,8 +551,14 @@ public class ProviderServiceTests
organization.PlanType = PlanType.EnterpriseMonthly; organization.PlanType = PlanType.EnterpriseMonthly;
organization.Plan = "Enterprise (Monthly)"; organization.Plan = "Enterprise (Monthly)";
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var expectedPlanType = PlanType.EnterpriseMonthly2020; var expectedPlanType = PlanType.EnterpriseMonthly2020;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(expectedPlanType)
.Returns(StaticStore.GetPlan(expectedPlanType));
var expectedPlanId = "2020-enterprise-org-seat-monthly"; var expectedPlanId = "2020-enterprise-org-seat-monthly";
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);

View File

@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Services.Contracts;
@ -128,6 +129,9 @@ public class ProviderBillingServiceTests
.GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId)) .GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId))
.Returns(existingPlan); .Returns(existingPlan);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(existingPlan.PlanType)
.Returns(StaticStore.GetPlan(existingPlan.PlanType));
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>(); var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.ProviderSubscriptionGetAsync( stripeAdapter.ProviderSubscriptionGetAsync(
Arg.Is(provider.GatewaySubscriptionId), Arg.Is(provider.GatewaySubscriptionId),
@ -156,6 +160,9 @@ public class ProviderBillingServiceTests
var command = var command =
new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId); new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(command.NewPlan)
.Returns(StaticStore.GetPlan(command.NewPlan));
// Act // Act
await sutProvider.Sut.ChangePlan(command); await sutProvider.Sut.ChangePlan(command);
@ -390,6 +397,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans); sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
// 50 seats currently assigned with a seat minimum of 100 // 50 seats currently assigned with a seat minimum of 100
@ -451,6 +464,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
var providerPlan = providerPlans.First(); var providerPlan = providerPlans.First();
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans); sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
@ -515,6 +534,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
var providerPlan = providerPlans.First(); var providerPlan = providerPlans.First();
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans); sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
@ -579,6 +604,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
var providerPlan = providerPlans.First(); var providerPlan = providerPlans.First();
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans); sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
@ -636,6 +667,8 @@ public class ProviderBillingServiceTests
} }
]); ]);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType));
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns( sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
[ [
new ProviderOrganizationOrganizationDetails new ProviderOrganizationOrganizationDetails
@ -672,6 +705,8 @@ public class ProviderBillingServiceTests
} }
]); ]);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType));
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns( sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
[ [
new ProviderOrganizationOrganizationDetails new ProviderOrganizationOrganizationDetails
@ -856,6 +891,9 @@ public class ProviderBillingServiceTests
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans); .Returns(providerPlans);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.EnterpriseMonthly)
.Returns(StaticStore.GetPlan(PlanType.EnterpriseMonthly));
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider)); await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
await sutProvider.GetDependency<IStripeAdapter>() await sutProvider.GetDependency<IStripeAdapter>()
@ -881,6 +919,9 @@ public class ProviderBillingServiceTests
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans); .Returns(providerPlans);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly)
.Returns(StaticStore.GetPlan(PlanType.TeamsMonthly));
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider)); await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
await sutProvider.GetDependency<IStripeAdapter>() await sutProvider.GetDependency<IStripeAdapter>()
@ -923,6 +964,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans); .Returns(providerPlans);
@ -968,6 +1015,12 @@ public class ProviderBillingServiceTests
} }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id) sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans); .Returns(providerPlans);
@ -1066,6 +1119,12 @@ public class ProviderBillingServiceTests
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 } new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
var command = new UpdateProviderSeatMinimumsCommand( var command = new UpdateProviderSeatMinimumsCommand(
@ -1139,6 +1198,12 @@ public class ProviderBillingServiceTests
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 15 } new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 15 }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
var command = new UpdateProviderSeatMinimumsCommand( var command = new UpdateProviderSeatMinimumsCommand(
@ -1212,6 +1277,12 @@ public class ProviderBillingServiceTests
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 } new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
var command = new UpdateProviderSeatMinimumsCommand( var command = new UpdateProviderSeatMinimumsCommand(
@ -1279,6 +1350,12 @@ public class ProviderBillingServiceTests
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 } new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
var command = new UpdateProviderSeatMinimumsCommand( var command = new UpdateProviderSeatMinimumsCommand(
@ -1352,6 +1429,12 @@ public class ProviderBillingServiceTests
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0 } new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0 }
}; };
foreach (var plan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
var command = new UpdateProviderSeatMinimumsCommand( var command = new UpdateProviderSeatMinimumsCommand(

View File

@ -10,6 +10,7 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -56,8 +57,8 @@ public class OrganizationsController : Controller
private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand; private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly IFeatureService _featureService;
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand; private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
private readonly IPricingClient _pricingClient;
public OrganizationsController( public OrganizationsController(
IOrganizationService organizationService, IOrganizationService organizationService,
@ -84,8 +85,8 @@ public class OrganizationsController : Controller
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
IFeatureService featureService, IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand) IPricingClient pricingClient)
{ {
_organizationService = organizationService; _organizationService = organizationService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -111,8 +112,8 @@ public class OrganizationsController : Controller
_providerOrganizationRepository = providerOrganizationRepository; _providerOrganizationRepository = providerOrganizationRepository;
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand; _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_featureService = featureService;
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand; _organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
_pricingClient = pricingClient;
} }
[RequirePermission(Permission.Org_List_View)] [RequirePermission(Permission.Org_List_View)]
@ -212,6 +213,8 @@ public class OrganizationsController : Controller
? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id) ? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
: -1; : -1;
var plans = await _pricingClient.ListPlans();
return View(new OrganizationEditModel( return View(new OrganizationEditModel(
organization, organization,
provider, provider,
@ -224,6 +227,7 @@ public class OrganizationsController : Controller
billingHistoryInfo, billingHistoryInfo,
billingSyncConnection, billingSyncConnection,
_globalSettings, _globalSettings,
plans,
secrets, secrets,
projects, projects,
serviceAccounts, serviceAccounts,
@ -253,8 +257,9 @@ public class OrganizationsController : Controller
UpdateOrganization(organization, model); UpdateOrganization(organization, model);
if (organization.UseSecretsManager && var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
{ {
TempData["Error"] = "Plan does not support Secrets Manager"; TempData["Error"] = "Plan does not support Secrets Manager";
return RedirectToAction("Edit", new { id }); return RedirectToAction("Edit", new { id });

View File

@ -8,6 +8,7 @@ using Bit.Core.Billing.Models;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.StaticStore;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
@ -17,6 +18,8 @@ namespace Bit.Admin.AdminConsole.Models;
public class OrganizationEditModel : OrganizationViewModel public class OrganizationEditModel : OrganizationViewModel
{ {
private readonly List<Plan> _plans;
public OrganizationEditModel() { } public OrganizationEditModel() { }
public OrganizationEditModel(Provider provider) public OrganizationEditModel(Provider provider)
@ -40,6 +43,7 @@ public class OrganizationEditModel : OrganizationViewModel
BillingHistoryInfo billingHistoryInfo, BillingHistoryInfo billingHistoryInfo,
IEnumerable<OrganizationConnection> connections, IEnumerable<OrganizationConnection> connections,
GlobalSettings globalSettings, GlobalSettings globalSettings,
List<Plan> plans,
int secrets, int secrets,
int projects, int projects,
int serviceAccounts, int serviceAccounts,
@ -96,6 +100,8 @@ public class OrganizationEditModel : OrganizationViewModel
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats; MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
SmServiceAccounts = org.SmServiceAccounts; SmServiceAccounts = org.SmServiceAccounts;
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts; MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
_plans = plans;
} }
public BillingInfo BillingInfo { get; set; } public BillingInfo BillingInfo { get; set; }
@ -183,7 +189,7 @@ public class OrganizationEditModel : OrganizationViewModel
* Add mappings for individual properties as you need them * Add mappings for individual properties as you need them
*/ */
public object GetPlansHelper() => public object GetPlansHelper() =>
StaticStore.Plans _plans
.Select(p => .Select(p =>
{ {
var plan = new var plan = new

View File

@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -55,6 +56,7 @@ public class OrganizationUsersController : Controller
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand; private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery; private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IPricingClient _pricingClient;
public OrganizationUsersController( public OrganizationUsersController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -77,7 +79,8 @@ public class OrganizationUsersController : Controller
IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand, IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery, IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
IFeatureService featureService) IFeatureService featureService,
IPricingClient pricingClient)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -100,6 +103,7 @@ public class OrganizationUsersController : Controller
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand; _deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery; _getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
_featureService = featureService; _featureService = featureService;
_pricingClient = pricingClient;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -648,7 +652,9 @@ public class OrganizationUsersController : Controller
if (additionalSmSeatsRequired > 0) if (additionalSmSeatsRequired > 0)
{ {
var organization = await _organizationRepository.GetByIdAsync(orgId); var organization = await _organizationRepository.GetByIdAsync(orgId);
var update = new SecretsManagerSubscriptionUpdate(organization, true) // TODO: https://bitwarden.atlassian.net/browse/PM-17000
var plan = await _pricingClient.GetPlanOrThrow(organization!.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true)
.AdjustSeats(additionalSmSeatsRequired); .AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
} }

View File

@ -22,6 +22,7 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services; using Bit.Core.Auth.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -60,6 +61,7 @@ public class OrganizationsController : Controller
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly IOrganizationDeleteCommand _organizationDeleteCommand; private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
private readonly IPricingClient _pricingClient;
public OrganizationsController( public OrganizationsController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -81,7 +83,8 @@ public class OrganizationsController : Controller
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory, IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRemoveOrganizationUserCommand removeOrganizationUserCommand,
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand, ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
IOrganizationDeleteCommand organizationDeleteCommand) IOrganizationDeleteCommand organizationDeleteCommand,
IPricingClient pricingClient)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -103,6 +106,7 @@ public class OrganizationsController : Controller
_removeOrganizationUserCommand = removeOrganizationUserCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand;
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand; _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
_organizationDeleteCommand = organizationDeleteCommand; _organizationDeleteCommand = organizationDeleteCommand;
_pricingClient = pricingClient;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -120,7 +124,8 @@ public class OrganizationsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
return new OrganizationResponseModel(organization); var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
} }
[HttpGet("")] [HttpGet("")]
@ -181,7 +186,8 @@ public class OrganizationsController : Controller
var organizationSignup = model.ToOrganizationSignup(user); var organizationSignup = model.ToOrganizationSignup(user);
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
return new OrganizationResponseModel(result.Organization); var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType);
return new OrganizationResponseModel(result.Organization, plan);
} }
[HttpPost("create-without-payment")] [HttpPost("create-without-payment")]
@ -196,7 +202,8 @@ public class OrganizationsController : Controller
var organizationSignup = model.ToOrganizationSignup(user); var organizationSignup = model.ToOrganizationSignup(user);
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
return new OrganizationResponseModel(result.Organization); var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType);
return new OrganizationResponseModel(result.Organization, plan);
} }
[HttpPut("{id}")] [HttpPut("{id}")]
@ -224,7 +231,8 @@ public class OrganizationsController : Controller
} }
await _organizationService.UpdateAsync(model.ToOrganization(organization, _globalSettings), updateBilling); await _organizationService.UpdateAsync(model.ToOrganization(organization, _globalSettings), updateBilling);
return new OrganizationResponseModel(organization); var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
} }
[HttpPost("{id}/storage")] [HttpPost("{id}/storage")]
@ -358,8 +366,8 @@ public class OrganizationsController : Controller
if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim) if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim)
{ {
// Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types // Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types
var plan = StaticStore.GetPlan(organization.PlanType); var productTier = organization.PlanType.GetProductTier();
if (plan.ProductTier is not ProductTierType.Enterprise and not ProductTierType.Teams) if (productTier is not ProductTierType.Enterprise and not ProductTierType.Teams)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -542,7 +550,8 @@ public class OrganizationsController : Controller
} }
await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated); await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
return new OrganizationResponseModel(organization); var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
} }
[HttpGet("{id}/plan-type")] [HttpGet("{id}/plan-type")]

View File

@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Constants = Bit.Core.Constants; using Constants = Bit.Core.Constants;
@ -11,8 +12,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class OrganizationResponseModel : ResponseModel public class OrganizationResponseModel : ResponseModel
{ {
public OrganizationResponseModel(Organization organization, string obj = "organization") public OrganizationResponseModel(
: base(obj) Organization organization,
Plan plan,
string obj = "organization") : base(obj)
{ {
if (organization == null) if (organization == null)
{ {
@ -28,7 +31,8 @@ public class OrganizationResponseModel : ResponseModel
BusinessCountry = organization.BusinessCountry; BusinessCountry = organization.BusinessCountry;
BusinessTaxNumber = organization.BusinessTaxNumber; BusinessTaxNumber = organization.BusinessTaxNumber;
BillingEmail = organization.BillingEmail; BillingEmail = organization.BillingEmail;
Plan = new PlanResponseModel(StaticStore.GetPlan(organization.PlanType)); // Self-Host instances only require plan information that can be derived from the Organization record.
Plan = plan != null ? new PlanResponseModel(plan) : new PlanResponseModel(organization);
PlanType = organization.PlanType; PlanType = organization.PlanType;
Seats = organization.Seats; Seats = organization.Seats;
MaxAutoscaleSeats = organization.MaxAutoscaleSeats; MaxAutoscaleSeats = organization.MaxAutoscaleSeats;
@ -110,7 +114,9 @@ public class OrganizationResponseModel : ResponseModel
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
{ {
public OrganizationSubscriptionResponseModel(Organization organization) : base(organization, "organizationSubscription") public OrganizationSubscriptionResponseModel(
Organization organization,
Plan plan) : base(organization, plan, "organizationSubscription")
{ {
Expiration = organization.ExpirationDate; Expiration = organization.ExpirationDate;
StorageName = organization.Storage.HasValue ? StorageName = organization.Storage.HasValue ?
@ -119,8 +125,11 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB
} }
public OrganizationSubscriptionResponseModel(Organization organization, SubscriptionInfo subscription, bool hideSensitiveData) public OrganizationSubscriptionResponseModel(
: this(organization) Organization organization,
SubscriptionInfo subscription,
Plan plan,
bool hideSensitiveData) : this(organization, plan)
{ {
Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null; Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null; UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
@ -142,7 +151,7 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
} }
public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) : public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) :
this(organization) this(organization, (Plan)null)
{ {
if (license != null) if (license != null)
{ {

View File

@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@ -37,7 +38,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
UsePasswordManager = organization.UsePasswordManager; UsePasswordManager = organization.UsePasswordManager;
UsersGetPremium = organization.UsersGetPremium; UsersGetPremium = organization.UsersGetPremium;
UseCustomPermissions = organization.UseCustomPermissions; UseCustomPermissions = organization.UseCustomPermissions;
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise; UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
SelfHost = organization.SelfHost; SelfHost = organization.SelfHost;
Seats = organization.Seats; Seats = organization.Seats;
MaxCollections = organization.MaxCollections; MaxCollections = organization.MaxCollections;
@ -60,7 +61,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
.UsersCanSponsor(organization); .UsersCanSponsor(organization);
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; ProductTierType = organization.PlanType.GetProductTier();
FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate; FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate;
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete; FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil; FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;

View File

@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Response; namespace Bit.Api.AdminConsole.Models.Response;
@ -26,7 +26,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
UseResetPassword = organization.UseResetPassword; UseResetPassword = organization.UseResetPassword;
UsersGetPremium = organization.UsersGetPremium; UsersGetPremium = organization.UsersGetPremium;
UseCustomPermissions = organization.UseCustomPermissions; UseCustomPermissions = organization.UseCustomPermissions;
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise; UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
SelfHost = organization.SelfHost; SelfHost = organization.SelfHost;
Seats = organization.Seats; Seats = organization.Seats;
MaxCollections = organization.MaxCollections; MaxCollections = organization.MaxCollections;
@ -44,7 +44,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
ProviderId = organization.ProviderId; ProviderId = organization.ProviderId;
ProviderName = organization.ProviderName; ProviderName = organization.ProviderName;
ProviderType = organization.ProviderType; ProviderType = organization.ProviderType;
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; ProductTierType = organization.PlanType.GetProductTier();
LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion; LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion; LimitItemDeletion = organization.LimitItemDeletion;

View File

@ -4,6 +4,7 @@ using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -21,6 +22,7 @@ public class OrganizationBillingController(
IOrganizationBillingService organizationBillingService, IOrganizationBillingService organizationBillingService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IPaymentService paymentService, IPaymentService paymentService,
IPricingClient pricingClient,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IPaymentHistoryService paymentHistoryService, IPaymentHistoryService paymentHistoryService,
IUserService userService) : BaseBillingController IUserService userService) : BaseBillingController
@ -279,7 +281,7 @@ public class OrganizationBillingController(
} }
var organizationSignup = model.ToOrganizationSignup(user); var organizationSignup = model.ToOrganizationSignup(user);
var sale = OrganizationSale.From(organization, organizationSignup); var sale = OrganizationSale.From(organization, organizationSignup);
var plan = StaticStore.GetPlan(model.PlanType); var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
sale.Organization.PlanType = plan.Type; sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name; sale.Organization.Plan = plan.Name;
sale.SubscriptionSetup.SkipTrial = true; sale.SubscriptionSetup.SkipTrial = true;

View File

@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
@ -45,7 +46,8 @@ public class OrganizationsController(
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand, IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
IReferenceEventService referenceEventService, IReferenceEventService referenceEventService,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IOrganizationInstallationRepository organizationInstallationRepository) IOrganizationInstallationRepository organizationInstallationRepository,
IPricingClient pricingClient)
: Controller : Controller
{ {
[HttpGet("{id:guid}/subscription")] [HttpGet("{id:guid}/subscription")]
@ -62,26 +64,28 @@ public class OrganizationsController(
throw new NotFoundException(); throw new NotFoundException();
} }
if (!globalSettings.SelfHosted && organization.Gateway != null)
{
var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization);
if (subscriptionInfo == null)
{
throw new NotFoundException();
}
var hideSensitiveData = !await currentContext.EditSubscription(id);
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, hideSensitiveData);
}
if (globalSettings.SelfHosted) if (globalSettings.SelfHosted)
{ {
var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization); var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);
return new OrganizationSubscriptionResponseModel(organization, orgLicense); return new OrganizationSubscriptionResponseModel(organization, orgLicense);
} }
return new OrganizationSubscriptionResponseModel(organization); var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
if (string.IsNullOrEmpty(organization.GatewaySubscriptionId))
{
return new OrganizationSubscriptionResponseModel(organization, plan);
}
var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization);
if (subscriptionInfo == null)
{
throw new NotFoundException();
}
var hideSensitiveData = !await currentContext.EditSubscription(id);
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, plan, hideSensitiveData);
} }
[HttpGet("{id:guid}/license")] [HttpGet("{id:guid}/license")]
@ -165,7 +169,8 @@ public class OrganizationsController(
organization = await AdjustOrganizationSeatsForSmTrialAsync(id, organization, model); organization = await AdjustOrganizationSeatsForSmTrialAsync(id, organization, model);
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization); var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization, plan);
await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate); await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);

View File

@ -2,6 +2,7 @@
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
@ -20,6 +21,7 @@ namespace Bit.Api.Billing.Controllers;
public class ProviderBillingController( public class ProviderBillingController(
ICurrentContext currentContext, ICurrentContext currentContext,
ILogger<BaseProviderController> logger, ILogger<BaseProviderController> logger,
IPricingClient pricingClient,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
IProviderPlanRepository providerPlanRepository, IProviderPlanRepository providerPlanRepository,
IProviderRepository providerRepository, IProviderRepository providerRepository,
@ -84,13 +86,25 @@ public class ProviderBillingController(
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
{
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
return new ConfiguredProviderPlan(
providerPlan.Id,
providerPlan.ProviderId,
plan,
providerPlan.SeatMinimum ?? 0,
providerPlan.PurchasedSeats ?? 0,
providerPlan.AllocatedSeats ?? 0);
}));
var taxInformation = GetTaxInformation(subscription.Customer); var taxInformation = GetTaxInformation(subscription.Customer);
var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription); var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription);
var response = ProviderSubscriptionResponse.From( var response = ProviderSubscriptionResponse.From(
subscription, subscription,
providerPlans, configuredProviderPlans,
taxInformation, taxInformation,
subscriptionSuspension, subscriptionSuspension,
provider); provider);

View File

@ -1,9 +1,7 @@
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Utilities;
using Stripe; using Stripe;
namespace Bit.Api.Billing.Models.Responses; namespace Bit.Api.Billing.Models.Responses;
@ -25,26 +23,24 @@ public record ProviderSubscriptionResponse(
public static ProviderSubscriptionResponse From( public static ProviderSubscriptionResponse From(
Subscription subscription, Subscription subscription,
ICollection<ProviderPlan> providerPlans, ICollection<ConfiguredProviderPlan> providerPlans,
TaxInformation taxInformation, TaxInformation taxInformation,
SubscriptionSuspension subscriptionSuspension, SubscriptionSuspension subscriptionSuspension,
Provider provider) Provider provider)
{ {
var providerPlanResponses = providerPlans var providerPlanResponses = providerPlans
.Where(providerPlan => providerPlan.IsConfigured()) .Select(providerPlan =>
.Select(ConfiguredProviderPlan.From)
.Select(configuredProviderPlan =>
{ {
var plan = StaticStore.GetPlan(configuredProviderPlan.PlanType); var plan = providerPlan.Plan;
var cost = (configuredProviderPlan.SeatMinimum + configuredProviderPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice; var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice;
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence; var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
return new ProviderPlanResponse( return new ProviderPlanResponse(
plan.Name, plan.Name,
plan.Type, plan.Type,
plan.ProductTier, plan.ProductTier,
configuredProviderPlan.SeatMinimum, providerPlan.SeatMinimum,
configuredProviderPlan.PurchasedSeats, providerPlan.PurchasedSeats,
configuredProviderPlan.AssignedSeats, providerPlan.AssignedSeats,
cost, cost,
cadence); cadence);
}); });

View File

@ -1,6 +1,7 @@
using System.Net; using System.Net;
using Bit.Api.Billing.Public.Models; using Bit.Api.Billing.Public.Models;
using Bit.Api.Models.Public.Response; using Bit.Api.Models.Public.Response;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -21,19 +22,22 @@ public class OrganizationController : Controller
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly ILogger<OrganizationController> _logger; private readonly ILogger<OrganizationController> _logger;
private readonly IPricingClient _pricingClient;
public OrganizationController( public OrganizationController(
IOrganizationService organizationService, IOrganizationService organizationService,
ICurrentContext currentContext, ICurrentContext currentContext,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
ILogger<OrganizationController> logger) ILogger<OrganizationController> logger,
IPricingClient pricingClient)
{ {
_organizationService = organizationService; _organizationService = organizationService;
_currentContext = currentContext; _currentContext = currentContext;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_logger = logger; _logger = logger;
_pricingClient = pricingClient;
} }
/// <summary> /// <summary>
@ -140,7 +144,8 @@ public class OrganizationController : Controller
return "Organization has no access to Secrets Manager."; return "Organization has no access to Secrets Manager.";
} }
var secretsManagerUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization); var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var secretsManagerUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization, plan);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(secretsManagerUpdate); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(secretsManagerUpdate);
return string.Empty; return string.Empty;

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
namespace Bit.Api.Billing.Public.Models; namespace Bit.Api.Billing.Public.Models;
@ -93,17 +94,17 @@ public class SecretsManagerSubscriptionUpdateModel
set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; } set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; }
} }
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization) public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan)
{ {
var update = UpdateUpdateMaxAutoScale(organization); var update = UpdateUpdateMaxAutoScale(organization, plan);
UpdateSeats(organization, update); UpdateSeats(organization, update);
UpdateServiceAccounts(organization, update); UpdateServiceAccounts(organization, update);
return update; return update;
} }
private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization) private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization, Plan plan)
{ {
var update = new SecretsManagerSubscriptionUpdate(organization, false) var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats, MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats,
MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts

View File

@ -1,5 +1,5 @@
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core.Utilities; using Bit.Core.Billing.Pricing;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -7,13 +7,15 @@ namespace Bit.Api.Controllers;
[Route("plans")] [Route("plans")]
[Authorize("Web")] [Authorize("Web")]
public class PlansController : Controller public class PlansController(
IPricingClient pricingClient) : Controller
{ {
[HttpGet("")] [HttpGet("")]
[AllowAnonymous] [AllowAnonymous]
public ListResponseModel<PlanResponseModel> Get() public async Task<ListResponseModel<PlanResponseModel>> Get()
{ {
var responses = StaticStore.Plans.Select(plan => new PlanResponseModel(plan)); var plans = await pricingClient.ListPlans();
var responses = plans.Select(plan => new PlanResponseModel(plan));
return new ListResponseModel<PlanResponseModel>(responses); return new ListResponseModel<PlanResponseModel>(responses);
} }
} }

View File

@ -64,7 +64,8 @@ public class SelfHostedOrganizationLicensesController : Controller
var result = await _organizationService.SignUpAsync(license, user, model.Key, var result = await _organizationService.SignUpAsync(license, user, model.Key,
model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey); model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey);
return new OrganizationResponseModel(result.Item1);
return new OrganizationResponseModel(result.Item1, null);
} }
[HttpPost("{id}")] [HttpPost("{id}")]

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
namespace Bit.Api.Models.Request.Organizations; namespace Bit.Api.Models.Request.Organizations;
@ -12,9 +13,9 @@ public class SecretsManagerSubscriptionUpdateRequestModel
public int ServiceAccountAdjustment { get; set; } public int ServiceAccountAdjustment { get; set; }
public int? MaxAutoscaleServiceAccounts { get; set; } public int? MaxAutoscaleServiceAccounts { get; set; }
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization) public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan)
{ {
return new SecretsManagerSubscriptionUpdate(organization, false) return new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
MaxAutoscaleSmSeats = MaxAutoscaleSeats, MaxAutoscaleSmSeats = MaxAutoscaleSeats,
MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts

View File

@ -1,4 +1,6 @@
using Bit.Core.Billing.Enums; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
@ -44,6 +46,13 @@ public class PlanResponseModel : ResponseModel
PasswordManager = new PasswordManagerPlanFeaturesResponseModel(plan.PasswordManager); PasswordManager = new PasswordManagerPlanFeaturesResponseModel(plan.PasswordManager);
} }
public PlanResponseModel(Organization organization, string obj = "plan") : base(obj)
{
Type = organization.PlanType;
ProductTier = organization.PlanType.GetProductTier();
Name = organization.Plan;
}
public PlanType Type { get; set; } public PlanType Type { get; set; }
public ProductTierType ProductTier { get; set; } public ProductTierType ProductTier { get; set; }
public string Name { get; set; } public string Name { get; set; }

View File

@ -1,6 +1,7 @@
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response; using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -37,6 +38,7 @@ public class ServiceAccountsController : Controller
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand; private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand; private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand;
private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand; private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand;
private readonly IPricingClient _pricingClient;
public ServiceAccountsController( public ServiceAccountsController(
ICurrentContext currentContext, ICurrentContext currentContext,
@ -52,7 +54,8 @@ public class ServiceAccountsController : Controller
ICreateServiceAccountCommand createServiceAccountCommand, ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand, IUpdateServiceAccountCommand updateServiceAccountCommand,
IDeleteServiceAccountsCommand deleteServiceAccountsCommand, IDeleteServiceAccountsCommand deleteServiceAccountsCommand,
IRevokeAccessTokensCommand revokeAccessTokensCommand) IRevokeAccessTokensCommand revokeAccessTokensCommand,
IPricingClient pricingClient)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_userService = userService; _userService = userService;
@ -66,6 +69,7 @@ public class ServiceAccountsController : Controller
_updateServiceAccountCommand = updateServiceAccountCommand; _updateServiceAccountCommand = updateServiceAccountCommand;
_deleteServiceAccountsCommand = deleteServiceAccountsCommand; _deleteServiceAccountsCommand = deleteServiceAccountsCommand;
_revokeAccessTokensCommand = revokeAccessTokensCommand; _revokeAccessTokensCommand = revokeAccessTokensCommand;
_pricingClient = pricingClient;
_createAccessTokenCommand = createAccessTokenCommand; _createAccessTokenCommand = createAccessTokenCommand;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
} }
@ -124,7 +128,9 @@ public class ServiceAccountsController : Controller
if (newServiceAccountSlotsRequired > 0) if (newServiceAccountSlotsRequired > 0)
{ {
var org = await _organizationRepository.GetByIdAsync(organizationId); var org = await _organizationRepository.GetByIdAsync(organizationId);
var update = new SecretsManagerSubscriptionUpdate(org, true) // TODO: https://bitwarden.atlassian.net/browse/PM-17002
var plan = await _pricingClient.GetPlanOrThrow(org!.PlanType);
var update = new SecretsManagerSubscriptionUpdate(org, plan, true)
.AdjustServiceAccounts(newServiceAccountSlotsRequired); .AdjustServiceAccounts(newServiceAccountSlotsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
} }

View File

@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -9,7 +10,6 @@ using Bit.Core.Services;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services; using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Event = Stripe.Event; using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations; namespace Bit.Billing.Services.Implementations;
@ -28,6 +28,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IStripeEventUtilityService _stripeEventUtilityService;
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationEnableCommand _organizationEnableCommand;
private readonly IPricingClient _pricingClient;
public PaymentSucceededHandler( public PaymentSucceededHandler(
ILogger<PaymentSucceededHandler> logger, ILogger<PaymentSucceededHandler> logger,
@ -41,7 +42,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
IStripeEventUtilityService stripeEventUtilityService, IStripeEventUtilityService stripeEventUtilityService,
IUserService userService, IUserService userService,
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
IOrganizationEnableCommand organizationEnableCommand) IOrganizationEnableCommand organizationEnableCommand,
IPricingClient pricingClient)
{ {
_logger = logger; _logger = logger;
_stripeEventService = stripeEventService; _stripeEventService = stripeEventService;
@ -55,6 +57,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
_userService = userService; _userService = userService;
_pushNotificationService = pushNotificationService; _pushNotificationService = pushNotificationService;
_organizationEnableCommand = organizationEnableCommand; _organizationEnableCommand = organizationEnableCommand;
_pricingClient = pricingClient;
} }
/// <summary> /// <summary>
@ -96,9 +99,9 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
return; return;
} }
var teamsMonthly = StaticStore.GetPlan(PlanType.TeamsMonthly); var teamsMonthly = await _pricingClient.GetPlanOrThrow(PlanType.TeamsMonthly);
var enterpriseMonthly = StaticStore.GetPlan(PlanType.EnterpriseMonthly); var enterpriseMonthly = await _pricingClient.GetPlanOrThrow(PlanType.EnterpriseMonthly);
var teamsMonthlyLineItem = var teamsMonthlyLineItem =
subscription.Items.Data.FirstOrDefault(item => subscription.Items.Data.FirstOrDefault(item =>
@ -137,14 +140,21 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
} }
else if (organizationId.HasValue) else if (organizationId.HasValue)
{ {
if (!subscription.Items.Any(i => var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
StaticStore.Plans.Any(p => p.PasswordManager.StripePlanId == i.Plan.Id)))
if (organization == null)
{
return;
}
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (subscription.Items.All(item => plan.PasswordManager.StripePlanId != item.Plan.Id))
{ {
return; return;
} }
await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);
await _referenceEventService.RaiseEventAsync( await _referenceEventService.RaiseEventAsync(

View File

@ -1,15 +1,17 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Repositories;
using Stripe; using Stripe;
namespace Bit.Billing.Services.Implementations; namespace Bit.Billing.Services.Implementations;
public class ProviderEventService( public class ProviderEventService(
ILogger<ProviderEventService> logger, IOrganizationRepository organizationRepository,
IPricingClient pricingClient,
IProviderInvoiceItemRepository providerInvoiceItemRepository, IProviderInvoiceItemRepository providerInvoiceItemRepository,
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IProviderPlanRepository providerPlanRepository, IProviderPlanRepository providerPlanRepository,
@ -54,7 +56,14 @@ public class ProviderEventService(
continue; continue;
} }
var plan = StaticStore.Plans.Single(x => x.Name == client.Plan && providerPlans.Any(y => y.PlanType == x.Type)); var organization = await organizationRepository.GetByIdAsync(client.OrganizationId);
if (organization == null)
{
return;
}
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
@ -76,7 +85,7 @@ public class ProviderEventService(
foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0)) foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0))
{ {
var plan = StaticStore.GetPlan(providerPlan.PlanType); var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
var clientSeats = invoiceItems var clientSeats = invoiceItems
.Where(item => item.PlanName == plan.Name) .Where(item => item.PlanName == plan.Name)

View File

@ -2,11 +2,11 @@
using Bit.Billing.Jobs; using Bit.Billing.Jobs;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Billing.Pricing;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Quartz; using Quartz;
using Stripe; using Stripe;
using Event = Stripe.Event; using Event = Stripe.Event;
@ -27,6 +27,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationEnableCommand _organizationEnableCommand;
private readonly IOrganizationDisableCommand _organizationDisableCommand; private readonly IOrganizationDisableCommand _organizationDisableCommand;
private readonly IPricingClient _pricingClient;
public SubscriptionUpdatedHandler( public SubscriptionUpdatedHandler(
IStripeEventService stripeEventService, IStripeEventService stripeEventService,
@ -40,7 +41,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
ISchedulerFactory schedulerFactory, ISchedulerFactory schedulerFactory,
IFeatureService featureService, IFeatureService featureService,
IOrganizationEnableCommand organizationEnableCommand, IOrganizationEnableCommand organizationEnableCommand,
IOrganizationDisableCommand organizationDisableCommand) IOrganizationDisableCommand organizationDisableCommand,
IPricingClient pricingClient)
{ {
_stripeEventService = stripeEventService; _stripeEventService = stripeEventService;
_stripeEventUtilityService = stripeEventUtilityService; _stripeEventUtilityService = stripeEventUtilityService;
@ -54,6 +56,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
_featureService = featureService; _featureService = featureService;
_organizationEnableCommand = organizationEnableCommand; _organizationEnableCommand = organizationEnableCommand;
_organizationDisableCommand = organizationDisableCommand; _organizationDisableCommand = organizationDisableCommand;
_pricingClient = pricingClient;
} }
/// <summary> /// <summary>
@ -156,7 +159,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
/// </summary> /// </summary>
/// <param name="parsedEvent"></param> /// <param name="parsedEvent"></param>
/// <param name="subscription"></param> /// <param name="subscription"></param>
private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(Event parsedEvent, private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(
Event parsedEvent,
Subscription subscription) Subscription subscription)
{ {
if (parsedEvent.Data.PreviousAttributes?.items is null) if (parsedEvent.Data.PreviousAttributes?.items is null)
@ -164,6 +168,22 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
return; return;
} }
var organization = subscription.Metadata.TryGetValue("organizationId", out var organizationId)
? await _organizationRepository.GetByIdAsync(Guid.Parse(organizationId))
: null;
if (organization == null)
{
return;
}
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (!plan.SupportsSecretsManager)
{
return;
}
var previousSubscription = parsedEvent.Data var previousSubscription = parsedEvent.Data
.PreviousAttributes .PreviousAttributes
.ToObject<Subscription>() as Subscription; .ToObject<Subscription>() as Subscription;
@ -171,17 +191,14 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
// This being false doesn't necessarily mean that the organization doesn't subscribe to Secrets Manager. // This being false doesn't necessarily mean that the organization doesn't subscribe to Secrets Manager.
// If there are changes to any subscription item, Stripe sends every item in the subscription, both // If there are changes to any subscription item, Stripe sends every item in the subscription, both
// changed and unchanged. // changed and unchanged.
var previousSubscriptionHasSecretsManager = previousSubscription?.Items is not null && var previousSubscriptionHasSecretsManager =
previousSubscription.Items.Any(previousItem => previousSubscription?.Items is not null &&
StaticStore.Plans.Any(p => previousSubscription.Items.Any(
p.SecretsManager is not null && previousSubscriptionItem => previousSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
p.SecretsManager.StripeSeatPlanId ==
previousItem.Plan.Id));
var currentSubscriptionHasSecretsManager = subscription.Items.Any(i => var currentSubscriptionHasSecretsManager =
StaticStore.Plans.Any(p => subscription.Items.Any(
p.SecretsManager is not null && currentSubscriptionItem => currentSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
p.SecretsManager.StripeSeatPlanId == i.Plan.Id));
if (!previousSubscriptionHasSecretsManager || currentSubscriptionHasSecretsManager) if (!previousSubscriptionHasSecretsManager || currentSubscriptionHasSecretsManager)
{ {

View File

@ -1,12 +1,11 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Stripe; using Stripe;
using Event = Stripe.Event; using Event = Stripe.Event;
@ -16,6 +15,7 @@ public class UpcomingInvoiceHandler(
ILogger<StripeEventProcessor> logger, ILogger<StripeEventProcessor> logger,
IMailService mailService, IMailService mailService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IPricingClient pricingClient,
IProviderRepository providerRepository, IProviderRepository providerRepository,
IStripeFacade stripeFacade, IStripeFacade stripeFacade,
IStripeEventService stripeEventService, IStripeEventService stripeEventService,
@ -52,7 +52,9 @@ public class UpcomingInvoiceHandler(
await TryEnableAutomaticTaxAsync(subscription); await TryEnableAutomaticTaxAsync(subscription);
if (!HasAnnualPlan(organization)) var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
if (!plan.IsAnnual)
{ {
return; return;
} }
@ -136,7 +138,7 @@ public class UpcomingInvoiceHandler(
{ {
if (subscription.AutomaticTax.Enabled || if (subscription.AutomaticTax.Enabled ||
!subscription.Customer.HasBillingLocation() || !subscription.Customer.HasBillingLocation() ||
IsNonTaxableNonUSBusinessUseSubscription(subscription)) await IsNonTaxableNonUSBusinessUseSubscription(subscription))
{ {
return; return;
} }
@ -150,14 +152,12 @@ public class UpcomingInvoiceHandler(
return; return;
bool IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription) async Task<bool> IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription)
{ {
var familyPriceIds = new List<string> var familyPriceIds = (await Task.WhenAll(
{ pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
// TODO: Replace with the PricingClient pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
StaticStore.GetPlan(PlanType.FamiliesAnnually2019).PasswordManager.StripePlanId, .Select(plan => plan.PasswordManager.StripePlanId);
StaticStore.GetPlan(PlanType.FamiliesAnnually).PasswordManager.StripePlanId
};
return localSubscription.Customer.Address.Country != "US" && return localSubscription.Customer.Address.Country != "US" &&
localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) && localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
@ -165,6 +165,4 @@ public class UpcomingInvoiceHandler(
!localSubscription.Customer.TaxIds.Any(); !localSubscription.Customer.TaxIds.Any();
} }
} }
private static bool HasAnnualPlan(Organization org) => StaticStore.GetPlan(org.PlanType).IsAnnual;
} }

View File

@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -24,6 +25,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
private readonly ICollectionRepository _collectionRepository; private readonly ICollectionRepository _collectionRepository;
private readonly IGroupRepository _groupRepository; private readonly IGroupRepository _groupRepository;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
public UpdateOrganizationUserCommand( public UpdateOrganizationUserCommand(
IEventService eventService, IEventService eventService,
@ -34,7 +36,8 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
IGroupRepository groupRepository, IGroupRepository groupRepository,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient)
{ {
_eventService = eventService; _eventService = eventService;
_organizationService = organizationService; _organizationService = organizationService;
@ -45,6 +48,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
_groupRepository = groupRepository; _groupRepository = groupRepository;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
} }
/// <summary> /// <summary>
@ -128,8 +132,10 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organizationUser.OrganizationId, 1); var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organizationUser.OrganizationId, 1);
if (additionalSmSeatsRequired > 0) if (additionalSmSeatsRequired > 0)
{ {
var update = new SecretsManagerSubscriptionUpdate(organization, true) // TODO: https://bitwarden.atlassian.net/browse/PM-17012
.AdjustSeats(additionalSmSeatsRequired); var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true)
.AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
} }
} }

View File

@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -45,11 +46,12 @@ public class CloudOrganizationSignUpCommand(
IPushRegistrationService pushRegistrationService, IPushRegistrationService pushRegistrationService,
IPushNotificationService pushNotificationService, IPushNotificationService pushNotificationService,
ICollectionRepository collectionRepository, ICollectionRepository collectionRepository,
IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand IDeviceRepository deviceRepository,
IPricingClient pricingClient) : ICloudOrganizationSignUpCommand
{ {
public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup) public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup)
{ {
var plan = StaticStore.GetPlan(signup.Plan); var plan = await pricingClient.GetPlanOrThrow(signup.Plan);
ValidatePasswordManagerPlan(plan, signup); ValidatePasswordManagerPlan(plan, signup);

View File

@ -16,6 +16,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -74,6 +75,7 @@ public class OrganizationService : IOrganizationService
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IOrganizationBillingService _organizationBillingService; private readonly IOrganizationBillingService _organizationBillingService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
public OrganizationService( public OrganizationService(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -108,7 +110,8 @@ public class OrganizationService : IOrganizationService
IFeatureService featureService, IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IOrganizationBillingService organizationBillingService, IOrganizationBillingService organizationBillingService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -143,6 +146,7 @@ public class OrganizationService : IOrganizationService
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_organizationBillingService = organizationBillingService; _organizationBillingService = organizationBillingService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
} }
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
@ -210,11 +214,7 @@ public class OrganizationService : IOrganizationService
throw new NotFoundException(); throw new NotFoundException();
} }
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (plan == null)
{
throw new BadRequestException("Existing plan not found.");
}
if (!plan.PasswordManager.HasAdditionalStorageOption) if (!plan.PasswordManager.HasAdditionalStorageOption)
{ {
@ -268,7 +268,7 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException($"Cannot set max seat autoscaling below current seat count."); throw new BadRequestException($"Cannot set max seat autoscaling below current seat count.");
} }
var plan = StaticStore.GetPlan(organization.PlanType); var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (plan == null) if (plan == null)
{ {
throw new BadRequestException("Existing plan not found."); throw new BadRequestException("Existing plan not found.");
@ -320,11 +320,7 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("No subscription found."); throw new BadRequestException("No subscription found.");
} }
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (plan == null)
{
throw new BadRequestException("Existing plan not found.");
}
if (!plan.PasswordManager.HasAdditionalSeatsOption) if (!plan.PasswordManager.HasAdditionalSeatsOption)
{ {
@ -442,7 +438,7 @@ public class OrganizationService : IOrganizationService
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup) public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup)
{ {
var plan = StaticStore.GetPlan(signup.Plan); var plan = await _pricingClient.GetPlanOrThrow(signup.Plan);
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager"); ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
@ -530,17 +526,6 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException(exception); throw new BadRequestException(exception);
} }
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType);
if (plan is null)
{
throw new BadRequestException($"Server must be updated to support {license.Plan}.");
}
if (license.PlanType != PlanType.Custom && plan.Disabled)
{
throw new BadRequestException($"Plan {plan.Name} is disabled.");
}
var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync(); var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();
if (enabledOrgs.Any(o => string.Equals(o.LicenseKey, license.LicenseKey))) if (enabledOrgs.Any(o => string.Equals(o.LicenseKey, license.LicenseKey)))
{ {
@ -882,7 +867,8 @@ public class OrganizationService : IOrganizationService
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount); var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount);
if (additionalSmSeatsRequired > 0) if (additionalSmSeatsRequired > 0)
{ {
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, true) var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, plan, true)
.AdjustSeats(additionalSmSeatsRequired); .AdjustSeats(additionalSmSeatsRequired);
} }
@ -1008,7 +994,8 @@ public class OrganizationService : IOrganizationService
if (initialSmSeatCount.HasValue && currentOrganization.SmSeats.HasValue && if (initialSmSeatCount.HasValue && currentOrganization.SmSeats.HasValue &&
currentOrganization.SmSeats.Value != initialSmSeatCount.Value) currentOrganization.SmSeats.Value != initialSmSeatCount.Value)
{ {
var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate(currentOrganization, false) var plan = await _pricingClient.GetPlanOrThrow(currentOrganization.PlanType);
var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate(currentOrganization, plan, false)
{ {
SmSeats = initialSmSeatCount.Value SmSeats = initialSmSeatCount.Value
}; };
@ -2237,13 +2224,6 @@ public class OrganizationService : IOrganizationService
public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted) public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted)
{ {
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
if (plan!.Disabled)
{
throw new BadRequestException("Plan not found.");
}
organization.Id = CoreHelpers.GenerateComb(); organization.Id = CoreHelpers.GenerateComb();
organization.Enabled = false; organization.Enabled = false;
organization.Status = OrganizationStatusType.Pending; organization.Status = OrganizationStatusType.Pending;

View File

@ -34,6 +34,7 @@ public static class StripeConstants
public static class InvoiceStatus public static class InvoiceStatus
{ {
public const string Draft = "draft"; public const string Draft = "draft";
public const string Open = "open";
} }
public static class MetadataKeys public static class MetadataKeys

View File

@ -10,6 +10,17 @@ namespace Bit.Core.Billing.Extensions;
public static class BillingExtensions public static class BillingExtensions
{ {
public static ProductTierType GetProductTier(this PlanType planType)
=> planType switch
{
PlanType.Custom or PlanType.Free => ProductTierType.Free,
PlanType.FamiliesAnnually or PlanType.FamiliesAnnually2019 => ProductTierType.Families,
PlanType.TeamsStarter or PlanType.TeamsStarter2023 => ProductTierType.TeamsStarter,
_ when planType.ToString().Contains("Teams") => ProductTierType.Teams,
_ when planType.ToString().Contains("Enterprise") => ProductTierType.Enterprise,
_ => throw new BillingException($"PlanType {planType} could not be matched to a ProductTierType")
};
public static bool IsBillable(this Provider provider) => public static bool IsBillable(this Provider provider) =>
provider is provider is
{ {

View File

@ -1,6 +1,7 @@
using Bit.Core.Billing.Caches; using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Caches.Implementations; using Bit.Core.Billing.Caches.Implementations;
using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Billing.Licenses.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations; using Bit.Core.Billing.Services.Implementations;
@ -17,7 +18,7 @@ public static class ServiceCollectionExtensions
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>(); services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>(); services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
services.AddTransient<ISubscriberService, SubscriberService>(); services.AddTransient<ISubscriberService, SubscriberService>();
// services.AddSingleton<IPricingClient, PricingClient>();
services.AddLicenseServices(); services.AddLicenseServices();
services.AddPricingClient();
} }
} }

View File

@ -3,11 +3,11 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Migration.Models; using Bit.Core.Billing.Migration.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Stripe; using Stripe;
using Plan = Bit.Core.Models.StaticStore.Plan; using Plan = Bit.Core.Models.StaticStore.Plan;
@ -19,6 +19,7 @@ public class OrganizationMigrator(
ILogger<OrganizationMigrator> logger, ILogger<OrganizationMigrator> logger,
IMigrationTrackerCache migrationTrackerCache, IMigrationTrackerCache migrationTrackerCache,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IPricingClient pricingClient,
IStripeAdapter stripeAdapter) : IOrganizationMigrator IStripeAdapter stripeAdapter) : IOrganizationMigrator
{ {
private const string _cancellationComment = "Cancelled as part of provider migration to Consolidated Billing"; private const string _cancellationComment = "Cancelled as part of provider migration to Consolidated Billing";
@ -137,7 +138,7 @@ public class OrganizationMigrator(
logger.LogInformation("CB: Bringing organization ({OrganizationID}) under provider management", logger.LogInformation("CB: Bringing organization ({OrganizationID}) under provider management",
organization.Id); organization.Id);
var plan = StaticStore.GetPlan(organization.Plan.Contains("Teams") ? PlanType.TeamsMonthly : PlanType.EnterpriseMonthly); var plan = await pricingClient.GetPlanOrThrow(organization.Plan.Contains("Teams") ? PlanType.TeamsMonthly : PlanType.EnterpriseMonthly);
ResetOrganizationPlan(organization, plan); ResetOrganizationPlan(organization, plan);
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
@ -206,7 +207,7 @@ public class OrganizationMigrator(
? StripeConstants.CollectionMethod.ChargeAutomatically ? StripeConstants.CollectionMethod.ChargeAutomatically
: StripeConstants.CollectionMethod.SendInvoice; : StripeConstants.CollectionMethod.SendInvoice;
var plan = StaticStore.GetPlan(organization.PlanType); var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
var items = new List<SubscriptionItemOptions> var items = new List<SubscriptionItemOptions>
{ {
@ -279,7 +280,7 @@ public class OrganizationMigrator(
throw new Exception(); throw new Exception();
} }
var plan = StaticStore.GetPlan(migrationRecord.PlanType); var plan = await pricingClient.GetPlanOrThrow(migrationRecord.PlanType);
ResetOrganizationPlan(organization, plan); ResetOrganizationPlan(organization, plan);
organization.MaxStorageGb = migrationRecord.MaxStorageGb; organization.MaxStorageGb = migrationRecord.MaxStorageGb;

View File

@ -1,24 +1,11 @@
using Bit.Core.Billing.Entities; using Bit.Core.Models.StaticStore;
using Bit.Core.Billing.Enums;
namespace Bit.Core.Billing.Models; namespace Bit.Core.Billing.Models;
public record ConfiguredProviderPlan( public record ConfiguredProviderPlan(
Guid Id, Guid Id,
Guid ProviderId, Guid ProviderId,
PlanType PlanType, Plan Plan,
int SeatMinimum, int SeatMinimum,
int PurchasedSeats, int PurchasedSeats,
int AssignedSeats) int AssignedSeats);
{
public static ConfiguredProviderPlan From(ProviderPlan providerPlan) =>
providerPlan.IsConfigured()
? new ConfiguredProviderPlan(
providerPlan.Id,
providerPlan.ProviderId,
providerPlan.PlanType,
providerPlan.SeatMinimum.GetValueOrDefault(0),
providerPlan.PurchasedSeats.GetValueOrDefault(0),
providerPlan.AllocatedSeats.GetValueOrDefault(0))
: null;
}

View File

@ -10,4 +10,17 @@ public record OrganizationMetadata(
bool IsSubscriptionCanceled, bool IsSubscriptionCanceled,
DateTime? InvoiceDueDate, DateTime? InvoiceDueDate,
DateTime? InvoiceCreatedDate, DateTime? InvoiceCreatedDate,
DateTime? SubPeriodEndDate); DateTime? SubPeriodEndDate)
{
public static OrganizationMetadata Default => new OrganizationMetadata(
false,
false,
false,
false,
false,
false,
false,
null,
null,
null);
}

View File

@ -76,8 +76,6 @@ public class OrganizationSale
private static SubscriptionSetup GetSubscriptionSetup(OrganizationUpgrade upgrade) private static SubscriptionSetup GetSubscriptionSetup(OrganizationUpgrade upgrade)
{ {
var plan = Core.Utilities.StaticStore.GetPlan(upgrade.Plan);
var passwordManagerOptions = new SubscriptionSetup.PasswordManager var passwordManagerOptions = new SubscriptionSetup.PasswordManager
{ {
Seats = upgrade.AdditionalSeats, Seats = upgrade.AdditionalSeats,
@ -95,7 +93,7 @@ public class OrganizationSale
return new SubscriptionSetup return new SubscriptionSetup
{ {
Plan = plan, PlanType = upgrade.Plan,
PasswordManagerOptions = passwordManagerOptions, PasswordManagerOptions = passwordManagerOptions,
SecretsManagerOptions = secretsManagerOptions SecretsManagerOptions = secretsManagerOptions
}; };

View File

@ -1,4 +1,4 @@
using Bit.Core.Models.StaticStore; using Bit.Core.Billing.Enums;
namespace Bit.Core.Billing.Models.Sales; namespace Bit.Core.Billing.Models.Sales;
@ -6,7 +6,7 @@ namespace Bit.Core.Billing.Models.Sales;
public class SubscriptionSetup public class SubscriptionSetup
{ {
public required Plan Plan { get; set; } public required PlanType PlanType { get; set; }
public required PasswordManager PasswordManagerOptions { get; set; } public required PasswordManager PasswordManagerOptions { get; set; }
public SecretsManager? SecretsManagerOptions { get; set; } public SecretsManager? SecretsManagerOptions { get; set; }
public bool SkipTrial = false; public bool SkipTrial = false;

View File

@ -1,5 +1,7 @@
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
using Bit.Core.Utilities;
#nullable enable #nullable enable
@ -7,6 +9,30 @@ namespace Bit.Core.Billing.Pricing;
public interface IPricingClient public interface IPricingClient
{ {
/// <summary>
/// Retrieve a Bitwarden plan by its <paramref name="planType"/>. If the feature flag 'use-pricing-service' is enabled,
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
/// </summary>
/// <param name="planType">The type of plan to retrieve.</param>
/// <returns>A Bitwarden <see cref="Plan"/> record or null in the case the plan could not be found or the method was executed from a self-hosted instance.</returns>
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
Task<Plan?> GetPlan(PlanType planType); Task<Plan?> GetPlan(PlanType planType);
/// <summary>
/// Retrieve a Bitwarden plan by its <paramref name="planType"/>. If the feature flag 'use-pricing-service' is enabled,
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
/// </summary>
/// <param name="planType">The type of plan to retrieve.</param>
/// <returns>A Bitwarden <see cref="Plan"/> record.</returns>
/// <exception cref="NotFoundException">Thrown when the <see cref="Plan"/> for the provided <paramref name="planType"/> could not be found or the method was executed from a self-hosted instance.</exception>
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
Task<Plan> GetPlanOrThrow(PlanType planType);
/// <summary>
/// Retrieve all the Bitwarden plans. If the feature flag 'use-pricing-service' is enabled,
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
/// </summary>
/// <returns>A list of Bitwarden <see cref="Plan"/> records or an empty list in the case the method is executed from a self-hosted instance.</returns>
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
Task<List<Plan>> ListPlans(); Task<List<Plan>> ListPlans();
} }

View File

@ -0,0 +1,35 @@
using System.Text.Json;
using Bit.Core.Billing.Pricing.Models;
namespace Bit.Core.Billing.Pricing.JSON;
#nullable enable
public class FreeOrScalableDTOJsonConverter : TypeReadingJsonConverter<FreeOrScalableDTO>
{
public override FreeOrScalableDTO? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var type = ReadType(reader);
return type switch
{
"free" => JsonSerializer.Deserialize<FreeDTO>(ref reader, options) switch
{
null => null,
var free => new FreeOrScalableDTO(free)
},
"scalable" => JsonSerializer.Deserialize<ScalableDTO>(ref reader, options) switch
{
null => null,
var scalable => new FreeOrScalableDTO(scalable)
},
_ => null
};
}
public override void Write(Utf8JsonWriter writer, FreeOrScalableDTO value, JsonSerializerOptions options)
=> value.Switch(
free => JsonSerializer.Serialize(writer, free, options),
scalable => JsonSerializer.Serialize(writer, scalable, options)
);
}

View File

@ -0,0 +1,40 @@
using System.Text.Json;
using Bit.Core.Billing.Pricing.Models;
namespace Bit.Core.Billing.Pricing.JSON;
#nullable enable
internal class PurchasableDTOJsonConverter : TypeReadingJsonConverter<PurchasableDTO>
{
public override PurchasableDTO? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var type = ReadType(reader);
return type switch
{
"free" => JsonSerializer.Deserialize<FreeDTO>(ref reader, options) switch
{
null => null,
var free => new PurchasableDTO(free)
},
"packaged" => JsonSerializer.Deserialize<PackagedDTO>(ref reader, options) switch
{
null => null,
var packaged => new PurchasableDTO(packaged)
},
"scalable" => JsonSerializer.Deserialize<ScalableDTO>(ref reader, options) switch
{
null => null,
var scalable => new PurchasableDTO(scalable)
},
_ => null
};
}
public override void Write(Utf8JsonWriter writer, PurchasableDTO value, JsonSerializerOptions options)
=> value.Switch(
free => JsonSerializer.Serialize(writer, free, options),
packaged => JsonSerializer.Serialize(writer, packaged, options),
scalable => JsonSerializer.Serialize(writer, scalable, options)
);
}

View File

@ -0,0 +1,28 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Bit.Core.Billing.Pricing.Models;
namespace Bit.Core.Billing.Pricing.JSON;
#nullable enable
public abstract class TypeReadingJsonConverter<T> : JsonConverter<T>
{
protected virtual string TypePropertyName => nameof(ScalableDTO.Type).ToLower();
protected string? ReadType(Utf8JsonReader reader)
{
while (reader.Read())
{
if (reader.TokenType != JsonTokenType.PropertyName || reader.GetString()?.ToLower() != TypePropertyName)
{
continue;
}
reader.Read();
return reader.GetString();
}
return null;
}
}

View File

@ -0,0 +1,9 @@
namespace Bit.Core.Billing.Pricing.Models;
#nullable enable
public class FeatureDTO
{
public string Name { get; set; } = null!;
public string LookupKey { get; set; } = null!;
}

View File

@ -0,0 +1,27 @@
namespace Bit.Core.Billing.Pricing.Models;
#nullable enable
public class PlanDTO
{
public string LookupKey { get; set; } = null!;
public string Name { get; set; } = null!;
public string Tier { get; set; } = null!;
public string? Cadence { get; set; }
public int? LegacyYear { get; set; }
public bool Available { get; set; }
public FeatureDTO[] Features { get; set; } = null!;
public PurchasableDTO Seats { get; set; } = null!;
public ScalableDTO? ManagedSeats { get; set; }
public ScalableDTO? Storage { get; set; }
public SecretsManagerPurchasablesDTO? SecretsManager { get; set; }
public int? TrialPeriodDays { get; set; }
public string[] CanUpgradeTo { get; set; } = null!;
public Dictionary<string, string> AdditionalData { get; set; } = null!;
}
public class SecretsManagerPurchasablesDTO
{
public FreeOrScalableDTO Seats { get; set; } = null!;
public FreeOrScalableDTO ServiceAccounts { get; set; } = null!;
}

View File

@ -0,0 +1,73 @@
using System.Text.Json.Serialization;
using Bit.Core.Billing.Pricing.JSON;
using OneOf;
namespace Bit.Core.Billing.Pricing.Models;
#nullable enable
[JsonConverter(typeof(PurchasableDTOJsonConverter))]
public class PurchasableDTO(OneOf<FreeDTO, PackagedDTO, ScalableDTO> input) : OneOfBase<FreeDTO, PackagedDTO, ScalableDTO>(input)
{
public static implicit operator PurchasableDTO(FreeDTO free) => new(free);
public static implicit operator PurchasableDTO(PackagedDTO packaged) => new(packaged);
public static implicit operator PurchasableDTO(ScalableDTO scalable) => new(scalable);
public T? FromFree<T>(Func<FreeDTO, T> select, Func<PurchasableDTO, T>? fallback = null) =>
IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default;
public T? FromPackaged<T>(Func<PackagedDTO, T> select, Func<PurchasableDTO, T>? fallback = null) =>
IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default;
public T? FromScalable<T>(Func<ScalableDTO, T> select, Func<PurchasableDTO, T>? fallback = null) =>
IsT2 ? select(AsT2) : fallback != null ? fallback(this) : default;
public bool IsFree => IsT0;
public bool IsPackaged => IsT1;
public bool IsScalable => IsT2;
}
[JsonConverter(typeof(FreeOrScalableDTOJsonConverter))]
public class FreeOrScalableDTO(OneOf<FreeDTO, ScalableDTO> input) : OneOfBase<FreeDTO, ScalableDTO>(input)
{
public static implicit operator FreeOrScalableDTO(FreeDTO freeDTO) => new(freeDTO);
public static implicit operator FreeOrScalableDTO(ScalableDTO scalableDTO) => new(scalableDTO);
public T? FromFree<T>(Func<FreeDTO, T> select, Func<FreeOrScalableDTO, T>? fallback = null) =>
IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default;
public T? FromScalable<T>(Func<ScalableDTO, T> select, Func<FreeOrScalableDTO, T>? fallback = null) =>
IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default;
public bool IsFree => IsT0;
public bool IsScalable => IsT1;
}
public class FreeDTO
{
public int Quantity { get; set; }
public string Type => "free";
}
public class PackagedDTO
{
public int Quantity { get; set; }
public string StripePriceId { get; set; } = null!;
public decimal Price { get; set; }
public AdditionalSeats? Additional { get; set; }
public string Type => "packaged";
public class AdditionalSeats
{
public string StripePriceId { get; set; } = null!;
public decimal Price { get; set; }
}
}
public class ScalableDTO
{
public int Provided { get; set; }
public string StripePriceId { get; set; } = null!;
public decimal Price { get; set; }
public string Type => "scalable";
}

View File

@ -1,6 +1,6 @@
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing.Models;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
using Proto.Billing.Pricing;
#nullable enable #nullable enable
@ -8,15 +8,15 @@ namespace Bit.Core.Billing.Pricing;
public record PlanAdapter : Plan public record PlanAdapter : Plan
{ {
public PlanAdapter(PlanResponse planResponse) public PlanAdapter(PlanDTO plan)
{ {
Type = ToPlanType(planResponse.LookupKey); Type = ToPlanType(plan.LookupKey);
ProductTier = ToProductTierType(Type); ProductTier = ToProductTierType(Type);
Name = planResponse.Name; Name = plan.Name;
IsAnnual = !string.IsNullOrEmpty(planResponse.Cadence) && planResponse.Cadence == "annually"; IsAnnual = plan.Cadence is "annually";
NameLocalizationKey = planResponse.AdditionalData?["nameLocalizationKey"]; NameLocalizationKey = plan.AdditionalData["nameLocalizationKey"];
DescriptionLocalizationKey = planResponse.AdditionalData?["descriptionLocalizationKey"]; DescriptionLocalizationKey = plan.AdditionalData["descriptionLocalizationKey"];
TrialPeriodDays = planResponse.TrialPeriodDays; TrialPeriodDays = plan.TrialPeriodDays;
HasSelfHost = HasFeature("selfHost"); HasSelfHost = HasFeature("selfHost");
HasPolicies = HasFeature("policies"); HasPolicies = HasFeature("policies");
HasGroups = HasFeature("groups"); HasGroups = HasFeature("groups");
@ -30,20 +30,20 @@ public record PlanAdapter : Plan
HasScim = HasFeature("scim"); HasScim = HasFeature("scim");
HasResetPassword = HasFeature("resetPassword"); HasResetPassword = HasFeature("resetPassword");
UsersGetPremium = HasFeature("usersGetPremium"); UsersGetPremium = HasFeature("usersGetPremium");
UpgradeSortOrder = planResponse.AdditionalData != null UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder)
? int.Parse(planResponse.AdditionalData["upgradeSortOrder"]) ? int.Parse(upgradeSortOrder)
: 0; : 0;
DisplaySortOrder = planResponse.AdditionalData != null DisplaySortOrder = plan.AdditionalData.TryGetValue("displaySortOrder", out var displaySortOrder)
? int.Parse(planResponse.AdditionalData["displaySortOrder"]) ? int.Parse(displaySortOrder)
: 0; : 0;
HasCustomPermissions = HasFeature("customPermissions"); Disabled = !plan.Available;
Disabled = !planResponse.Available; LegacyYear = plan.LegacyYear;
PasswordManager = ToPasswordManagerPlanFeatures(planResponse); PasswordManager = ToPasswordManagerPlanFeatures(plan);
SecretsManager = planResponse.SecretsManager != null ? ToSecretsManagerPlanFeatures(planResponse) : null; SecretsManager = plan.SecretsManager != null ? ToSecretsManagerPlanFeatures(plan) : null;
return; return;
bool HasFeature(string lookupKey) => planResponse.Features.Any(feature => feature.LookupKey == lookupKey); bool HasFeature(string lookupKey) => plan.Features.Any(feature => feature.LookupKey == lookupKey);
} }
#region Mappings #region Mappings
@ -86,29 +86,25 @@ public record PlanAdapter : Plan
_ => throw new BillingException() // TODO: Flesh out _ => throw new BillingException() // TODO: Flesh out
}; };
private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(PlanResponse planResponse) private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(PlanDTO plan)
{ {
var stripePlanId = GetStripePlanId(planResponse.Seats); var stripePlanId = GetStripePlanId(plan.Seats);
var stripeSeatPlanId = GetStripeSeatPlanId(planResponse.Seats); var stripeSeatPlanId = GetStripeSeatPlanId(plan.Seats);
var stripeProviderPortalSeatPlanId = planResponse.ManagedSeats?.StripePriceId; var stripeProviderPortalSeatPlanId = plan.ManagedSeats?.StripePriceId;
var basePrice = GetBasePrice(planResponse.Seats); var basePrice = GetBasePrice(plan.Seats);
var seatPrice = GetSeatPrice(planResponse.Seats); var seatPrice = GetSeatPrice(plan.Seats);
var providerPortalSeatPrice = var providerPortalSeatPrice = plan.ManagedSeats?.Price ?? 0;
planResponse.ManagedSeats != null ? decimal.Parse(planResponse.ManagedSeats.Price) : 0; var scales = plan.Seats.Match(
var scales = planResponse.Seats.KindCase switch _ => false,
{ packaged => packaged.Additional != null,
PurchasableDTO.KindOneofCase.Scalable => true, _ => true);
PurchasableDTO.KindOneofCase.Packaged => planResponse.Seats.Packaged.Additional != null, var baseSeats = GetBaseSeats(plan.Seats);
_ => false var maxSeats = GetMaxSeats(plan.Seats);
}; var baseStorageGb = (short?)plan.Storage?.Provided;
var baseSeats = GetBaseSeats(planResponse.Seats); var hasAdditionalStorageOption = plan.Storage != null;
var maxSeats = GetMaxSeats(planResponse.Seats); var additionalStoragePricePerGb = plan.Storage?.Price ?? 0;
var baseStorageGb = (short?)planResponse.Storage?.Provided; var stripeStoragePlanId = plan.Storage?.StripePriceId;
var hasAdditionalStorageOption = planResponse.Storage != null; short? maxCollections = plan.AdditionalData.TryGetValue("passwordManager.maxCollections", out var value) ? short.Parse(value) : null;
var stripeStoragePlanId = planResponse.Storage?.StripePriceId;
short? maxCollections =
planResponse.AdditionalData != null &&
planResponse.AdditionalData.TryGetValue("passwordManager.maxCollections", out var value) ? short.Parse(value) : null;
return new PasswordManagerPlanFeatures return new PasswordManagerPlanFeatures
{ {
@ -124,30 +120,29 @@ public record PlanAdapter : Plan
MaxSeats = maxSeats, MaxSeats = maxSeats,
BaseStorageGb = baseStorageGb, BaseStorageGb = baseStorageGb,
HasAdditionalStorageOption = hasAdditionalStorageOption, HasAdditionalStorageOption = hasAdditionalStorageOption,
AdditionalStoragePricePerGb = additionalStoragePricePerGb,
StripeStoragePlanId = stripeStoragePlanId, StripeStoragePlanId = stripeStoragePlanId,
MaxCollections = maxCollections MaxCollections = maxCollections
}; };
} }
private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(PlanResponse planResponse) private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(PlanDTO plan)
{ {
var seats = planResponse.SecretsManager.Seats; var seats = plan.SecretsManager!.Seats;
var serviceAccounts = planResponse.SecretsManager.ServiceAccounts; var serviceAccounts = plan.SecretsManager.ServiceAccounts;
var maxServiceAccounts = GetMaxServiceAccounts(serviceAccounts); var maxServiceAccounts = GetMaxServiceAccounts(serviceAccounts);
var allowServiceAccountsAutoscale = serviceAccounts.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable; var allowServiceAccountsAutoscale = serviceAccounts.IsScalable;
var stripeServiceAccountPlanId = GetStripeServiceAccountPlanId(serviceAccounts); var stripeServiceAccountPlanId = GetStripeServiceAccountPlanId(serviceAccounts);
var additionalPricePerServiceAccount = GetAdditionalPricePerServiceAccount(serviceAccounts); var additionalPricePerServiceAccount = GetAdditionalPricePerServiceAccount(serviceAccounts);
var baseServiceAccount = GetBaseServiceAccount(serviceAccounts); var baseServiceAccount = GetBaseServiceAccount(serviceAccounts);
var hasAdditionalServiceAccountOption = serviceAccounts.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable; var hasAdditionalServiceAccountOption = serviceAccounts.IsScalable;
var stripeSeatPlanId = GetStripeSeatPlanId(seats); var stripeSeatPlanId = GetStripeSeatPlanId(seats);
var hasAdditionalSeatsOption = seats.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable; var hasAdditionalSeatsOption = seats.IsScalable;
var seatPrice = GetSeatPrice(seats); var seatPrice = GetSeatPrice(seats);
var maxSeats = GetMaxSeats(seats); var maxSeats = GetMaxSeats(seats);
var allowSeatAutoscale = seats.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable; var allowSeatAutoscale = seats.IsScalable;
var maxProjects = var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0;
planResponse.AdditionalData != null &&
planResponse.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0;
return new SecretsManagerPlanFeatures return new SecretsManagerPlanFeatures
{ {
@ -167,66 +162,54 @@ public record PlanAdapter : Plan
} }
private static decimal? GetAdditionalPricePerServiceAccount(FreeOrScalableDTO freeOrScalable) private static decimal? GetAdditionalPricePerServiceAccount(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable => freeOrScalable.FromScalable(x => x.Price);
? null
: decimal.Parse(freeOrScalable.Scalable.Price);
private static decimal GetBasePrice(PurchasableDTO purchasable) private static decimal GetBasePrice(PurchasableDTO purchasable)
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? 0 : decimal.Parse(purchasable.Packaged.Price); => purchasable.FromPackaged(x => x.Price);
private static int GetBaseSeats(PurchasableDTO purchasable) private static int GetBaseSeats(PurchasableDTO purchasable)
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? 0 : purchasable.Packaged.Quantity; => purchasable.FromPackaged(x => x.Quantity);
private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable) private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase switch => freeOrScalable.Match(
{ free => (short)free.Quantity,
FreeOrScalableDTO.KindOneofCase.Free => (short)freeOrScalable.Free.Quantity, scalable => (short)scalable.Provided);
FreeOrScalableDTO.KindOneofCase.Scalable => (short)freeOrScalable.Scalable.Provided,
_ => 0
};
private static short? GetMaxSeats(PurchasableDTO purchasable) private static short? GetMaxSeats(PurchasableDTO purchasable)
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Free ? null : (short)purchasable.Free.Quantity; => purchasable.Match<short?>(
free => (short)free.Quantity,
packaged => (short)packaged.Quantity,
_ => null);
private static short? GetMaxSeats(FreeOrScalableDTO freeOrScalable) private static short? GetMaxSeats(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Free ? null : (short)freeOrScalable.Free.Quantity; => freeOrScalable.FromFree(x => (short)x.Quantity);
private static short? GetMaxServiceAccounts(FreeOrScalableDTO freeOrScalable) private static short? GetMaxServiceAccounts(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Free ? null : (short)freeOrScalable.Free.Quantity; => freeOrScalable.FromFree(x => (short)x.Quantity);
private static decimal GetSeatPrice(PurchasableDTO purchasable) private static decimal GetSeatPrice(PurchasableDTO purchasable)
=> purchasable.KindCase switch => purchasable.Match(
{ _ => 0,
PurchasableDTO.KindOneofCase.Packaged => purchasable.Packaged.Additional != null ? decimal.Parse(purchasable.Packaged.Additional.Price) : 0, packaged => packaged.Additional?.Price ?? 0,
PurchasableDTO.KindOneofCase.Scalable => decimal.Parse(purchasable.Scalable.Price), scalable => scalable.Price);
_ => 0
};
private static decimal GetSeatPrice(FreeOrScalableDTO freeOrScalable) private static decimal GetSeatPrice(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable => freeOrScalable.FromScalable(x => x.Price);
? 0
: decimal.Parse(freeOrScalable.Scalable.Price);
private static string? GetStripePlanId(PurchasableDTO purchasable) private static string? GetStripePlanId(PurchasableDTO purchasable)
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? null : purchasable.Packaged.StripePriceId; => purchasable.FromPackaged(x => x.StripePriceId);
private static string? GetStripeSeatPlanId(PurchasableDTO purchasable) private static string? GetStripeSeatPlanId(PurchasableDTO purchasable)
=> purchasable.KindCase switch => purchasable.Match(
{ _ => null,
PurchasableDTO.KindOneofCase.Packaged => purchasable.Packaged.Additional?.StripePriceId, packaged => packaged.Additional?.StripePriceId,
PurchasableDTO.KindOneofCase.Scalable => purchasable.Scalable.StripePriceId, scalable => scalable.StripePriceId);
_ => null
};
private static string? GetStripeSeatPlanId(FreeOrScalableDTO freeOrScalable) private static string? GetStripeSeatPlanId(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable => freeOrScalable.FromScalable(x => x.StripePriceId);
? null
: freeOrScalable.Scalable.StripePriceId;
private static string? GetStripeServiceAccountPlanId(FreeOrScalableDTO freeOrScalable) private static string? GetStripeServiceAccountPlanId(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable => freeOrScalable.FromScalable(x => x.StripePriceId);
? null
: freeOrScalable.Scalable.StripePriceId;
#endregion #endregion
} }

View File

@ -1,12 +1,13 @@
using Bit.Core.Billing.Enums; using System.Net;
using Bit.Core.Models.StaticStore; using System.Net.Http.Json;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing.Models;
using Bit.Core.Exceptions;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Google.Protobuf.WellKnownTypes; using Microsoft.Extensions.Logging;
using Grpc.Core; using Plan = Bit.Core.Models.StaticStore.Plan;
using Grpc.Net.Client;
using Proto.Billing.Pricing;
#nullable enable #nullable enable
@ -14,10 +15,17 @@ namespace Bit.Core.Billing.Pricing;
public class PricingClient( public class PricingClient(
IFeatureService featureService, IFeatureService featureService,
GlobalSettings globalSettings) : IPricingClient GlobalSettings globalSettings,
HttpClient httpClient,
ILogger<PricingClient> logger) : IPricingClient
{ {
public async Task<Plan?> GetPlan(PlanType planType) public async Task<Plan?> GetPlan(PlanType planType)
{ {
if (globalSettings.SelfHosted)
{
return null;
}
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService); var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
if (!usePricingService) if (!usePricingService)
@ -25,30 +33,55 @@ public class PricingClient(
return StaticStore.GetPlan(planType); return StaticStore.GetPlan(planType);
} }
using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri); var lookupKey = GetLookupKey(planType);
var client = new PasswordManager.PasswordManagerClient(channel);
var lookupKey = ToLookupKey(planType); if (lookupKey == null)
if (string.IsNullOrEmpty(lookupKey))
{ {
logger.LogError("Could not find Pricing Service lookup key for PlanType {PlanType}", planType);
return null; return null;
} }
try var response = await httpClient.GetAsync($"plans/lookup/{lookupKey}");
{
var response =
await client.GetPlanByLookupKeyAsync(new GetPlanByLookupKeyRequest { LookupKey = lookupKey });
return new PlanAdapter(response); if (response.IsSuccessStatusCode)
}
catch (RpcException rpcException) when (rpcException.StatusCode == StatusCode.NotFound)
{ {
var plan = await response.Content.ReadFromJsonAsync<PlanDTO>();
if (plan == null)
{
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
}
return new PlanAdapter(plan);
}
if (response.StatusCode == HttpStatusCode.NotFound)
{
logger.LogError("Pricing Service plan for PlanType {PlanType} was not found", planType);
return null; return null;
} }
throw new BillingException(
message: $"Request to the Pricing Service failed with status code {response.StatusCode}");
}
public async Task<Plan> GetPlanOrThrow(PlanType planType)
{
var plan = await GetPlan(planType);
if (plan == null)
{
throw new NotFoundException();
}
return plan;
} }
public async Task<List<Plan>> ListPlans() public async Task<List<Plan>> ListPlans()
{ {
if (globalSettings.SelfHosted)
{
return [];
}
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService); var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
if (!usePricingService) if (!usePricingService)
@ -56,14 +89,23 @@ public class PricingClient(
return StaticStore.Plans.ToList(); return StaticStore.Plans.ToList();
} }
using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri); var response = await httpClient.GetAsync("plans");
var client = new PasswordManager.PasswordManagerClient(channel);
var response = await client.ListPlansAsync(new Empty()); if (response.IsSuccessStatusCode)
return response.Plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList(); {
var plans = await response.Content.ReadFromJsonAsync<List<PlanDTO>>();
if (plans == null)
{
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
}
return plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList();
}
throw new BillingException(
message: $"Request to the Pricing Service failed with status {response.StatusCode}");
} }
private static string? ToLookupKey(PlanType planType) private static string? GetLookupKey(PlanType planType)
=> planType switch => planType switch
{ {
PlanType.EnterpriseAnnually => "enterprise-annually", PlanType.EnterpriseAnnually => "enterprise-annually",

View File

@ -1,92 +0,0 @@
syntax = "proto3";
option csharp_namespace = "Proto.Billing.Pricing";
package plans;
import "google/protobuf/empty.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/wrappers.proto";
service PasswordManager {
rpc GetPlanByLookupKey (GetPlanByLookupKeyRequest) returns (PlanResponse);
rpc ListPlans (google.protobuf.Empty) returns (ListPlansResponse);
}
// Requests
message GetPlanByLookupKeyRequest {
string lookupKey = 1;
}
// Responses
message PlanResponse {
string name = 1;
string lookupKey = 2;
string tier = 4;
optional string cadence = 6;
optional google.protobuf.Int32Value legacyYear = 8;
bool available = 9;
repeated FeatureDTO features = 10;
PurchasableDTO seats = 11;
optional ScalableDTO managedSeats = 12;
optional ScalableDTO storage = 13;
optional SecretsManagerPurchasablesDTO secretsManager = 14;
optional google.protobuf.Int32Value trialPeriodDays = 15;
repeated string canUpgradeTo = 16;
map<string, string> additionalData = 17;
}
message ListPlansResponse {
repeated PlanResponse plans = 1;
}
// DTOs
message FeatureDTO {
string name = 1;
string lookupKey = 2;
}
message FreeDTO {
int32 quantity = 2;
string type = 4;
}
message PackagedDTO {
message AdditionalSeats {
string stripePriceId = 1;
string price = 2;
}
int32 quantity = 2;
string stripePriceId = 3;
string price = 4;
optional AdditionalSeats additional = 5;
string type = 6;
}
message ScalableDTO {
int32 provided = 2;
string stripePriceId = 6;
string price = 7;
string type = 9;
}
message PurchasableDTO {
oneof kind {
FreeDTO free = 1;
PackagedDTO packaged = 2;
ScalableDTO scalable = 3;
}
}
message FreeOrScalableDTO {
oneof kind {
FreeDTO free = 1;
ScalableDTO scalable = 2;
}
}
message SecretsManagerPurchasablesDTO {
FreeOrScalableDTO seats = 1;
FreeOrScalableDTO serviceAccounts = 2;
}

View File

@ -0,0 +1,21 @@
using Bit.Core.Settings;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Billing.Pricing;
public static class ServiceCollectionExtensions
{
public static void AddPricingClient(this IServiceCollection services)
{
services.AddHttpClient<IPricingClient, PricingClient>((serviceProvider, httpClient) =>
{
var globalSettings = serviceProvider.GetRequiredService<GlobalSettings>();
if (string.IsNullOrEmpty(globalSettings.PricingUri))
{
return;
}
httpClient.BaseAddress = new Uri(globalSettings.PricingUri);
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
});
}
}

View File

@ -3,12 +3,12 @@ using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
using Braintree; using Braintree;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Stripe; using Stripe;
@ -26,6 +26,7 @@ public class OrganizationBillingService(
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
ILogger<OrganizationBillingService> logger, ILogger<OrganizationBillingService> logger,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IPricingClient pricingClient,
ISetupIntentCache setupIntentCache, ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
ISubscriberService subscriberService, ISubscriberService subscriberService,
@ -63,13 +64,22 @@ public class OrganizationBillingService(
return null; return null;
} }
var isEligibleForSelfHost = IsEligibleForSelfHost(organization); if (globalSettings.SelfHosted)
{
return OrganizationMetadata.Default;
}
var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization);
var isManaged = organization.Status == OrganizationStatusType.Managed; var isManaged = organization.Status == OrganizationStatusType.Managed;
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{ {
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false, return OrganizationMetadata.Default with
false, false, false, false, null, null, null); {
IsEligibleForSelfHost = isEligibleForSelfHost,
IsManaged = isManaged
};
} }
var customer = await subscriberService.GetCustomer(organization, var customer = await subscriberService.GetCustomer(organization,
@ -77,18 +87,21 @@ public class OrganizationBillingService(
var subscription = await subscriberService.GetSubscription(organization); var subscription = await subscriberService.GetSubscription(organization);
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription); var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription);
var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription);
var isSubscriptionCanceled = IsSubscriptionCanceled(subscription);
var hasSubscription = true;
var openInvoice = await HasOpenInvoiceAsync(subscription);
var hasOpenInvoice = openInvoice.HasOpenInvoice;
var invoiceDueDate = openInvoice.DueDate;
var invoiceCreatedDate = openInvoice.CreatedDate;
var subPeriodEndDate = subscription?.CurrentPeriodEnd;
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone, var invoice = await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions());
isSubscriptionUnpaid, hasSubscription, hasOpenInvoice, isSubscriptionCanceled, invoiceDueDate, invoiceCreatedDate, subPeriodEndDate);
return new OrganizationMetadata(
isEligibleForSelfHost,
isManaged,
isOnSecretsManagerStandalone,
subscription.Status == StripeConstants.SubscriptionStatus.Unpaid,
true,
invoice?.Status == StripeConstants.InvoiceStatus.Open,
subscription.Status == StripeConstants.SubscriptionStatus.Canceled,
invoice?.DueDate,
invoice?.Created,
subscription.CurrentPeriodEnd);
} }
public async Task UpdatePaymentMethod( public async Task UpdatePaymentMethod(
@ -299,7 +312,7 @@ public class OrganizationBillingService(
Customer customer, Customer customer,
SubscriptionSetup subscriptionSetup) SubscriptionSetup subscriptionSetup)
{ {
var plan = subscriptionSetup.Plan; var plan = await pricingClient.GetPlanOrThrow(subscriptionSetup.PlanType);
var passwordManagerOptions = subscriptionSetup.PasswordManagerOptions; var passwordManagerOptions = subscriptionSetup.PasswordManagerOptions;
@ -385,15 +398,17 @@ public class OrganizationBillingService(
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
} }
private static bool IsEligibleForSelfHost( private async Task<bool> IsEligibleForSelfHostAsync(
Organization organization) Organization organization)
{ {
var eligibleSelfHostPlans = StaticStore.Plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type); var plans = await pricingClient.ListPlans();
var eligibleSelfHostPlans = plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type);
return eligibleSelfHostPlans.Contains(organization.PlanType); return eligibleSelfHostPlans.Contains(organization.PlanType);
} }
private static bool IsOnSecretsManagerStandalone( private async Task<bool> IsOnSecretsManagerStandalone(
Organization organization, Organization organization,
Customer? customer, Customer? customer,
Subscription? subscription) Subscription? subscription)
@ -403,7 +418,7 @@ public class OrganizationBillingService(
return false; return false;
} }
var plan = StaticStore.GetPlan(organization.PlanType); var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
if (!plan.SupportsSecretsManager) if (!plan.SupportsSecretsManager)
{ {
@ -424,38 +439,5 @@ public class OrganizationBillingService(
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any(); return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
} }
private static bool IsSubscriptionUnpaid(Subscription subscription)
{
if (subscription == null)
{
return false;
}
return subscription.Status == "unpaid";
}
private async Task<(bool HasOpenInvoice, DateTime? CreatedDate, DateTime? DueDate)> HasOpenInvoiceAsync(Subscription subscription)
{
if (subscription?.LatestInvoiceId == null)
{
return (false, null, null);
}
var invoice = await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions());
return invoice?.Status == "open"
? (true, invoice.Created, invoice.DueDate)
: (false, null, null);
}
private static bool IsSubscriptionCanceled(Subscription subscription)
{
if (subscription == null)
{
return false;
}
return subscription.Status == "canceled";
}
#endregion #endregion
} }

View File

@ -27,12 +27,6 @@
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.85" /> <PackageReference Include="AWSSDK.SQS" Version="3.7.400.85" />
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" /> <PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" /> <PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
<PackageReference Include="Grpc.Tools" Version="2.68.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" /> <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" /> <PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" /> <PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
@ -78,11 +72,7 @@
<PackageReference Include="System.Text.Json" Version="8.0.5" /> <PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Protobuf Include="Billing\Pricing\Protos\password-manager.proto" GrpcServices="Client" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Resources\" /> <Folder Include="Resources\" />
<Folder Include="Properties\" /> <Folder Include="Properties\" />

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Stripe; using Stripe;
using Plan = Bit.Core.Models.StaticStore.Plan;
namespace Bit.Core.Models.Business; namespace Bit.Core.Models.Business;
@ -9,7 +10,7 @@ namespace Bit.Core.Models.Business;
/// </summary> /// </summary>
public class SubscriptionData public class SubscriptionData
{ {
public StaticStore.Plan Plan { get; init; } public Plan Plan { get; init; }
public int PurchasedPasswordManagerSeats { get; init; } public int PurchasedPasswordManagerSeats { get; init; }
public bool SubscribedToSecretsManager { get; set; } public bool SubscribedToSecretsManager { get; set; }
public int? PurchasedSecretsManagerSeats { get; init; } public int? PurchasedSecretsManagerSeats { get; init; }
@ -38,22 +39,24 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
/// in the case of an error. /// in the case of an error.
/// </summary> /// </summary>
/// <param name="organization">The <see cref="Organization"/> to upgrade.</param> /// <param name="organization">The <see cref="Organization"/> to upgrade.</param>
/// <param name="plan">The organization's plan.</param>
/// <param name="updatedSubscription">The updates you want to apply to the organization's subscription.</param> /// <param name="updatedSubscription">The updates you want to apply to the organization's subscription.</param>
public CompleteSubscriptionUpdate( public CompleteSubscriptionUpdate(
Organization organization, Organization organization,
Plan plan,
SubscriptionData updatedSubscription) SubscriptionData updatedSubscription)
{ {
_currentSubscription = GetSubscriptionDataFor(organization); _currentSubscription = GetSubscriptionDataFor(organization, plan);
_updatedSubscription = updatedSubscription; _updatedSubscription = updatedSubscription;
} }
protected override List<string> PlanIds => new() protected override List<string> PlanIds =>
{ [
GetPasswordManagerPlanId(_updatedSubscription.Plan), GetPasswordManagerPlanId(_updatedSubscription.Plan),
_updatedSubscription.Plan.SecretsManager.StripeSeatPlanId, _updatedSubscription.Plan.SecretsManager.StripeSeatPlanId,
_updatedSubscription.Plan.SecretsManager.StripeServiceAccountPlanId, _updatedSubscription.Plan.SecretsManager.StripeServiceAccountPlanId,
_updatedSubscription.Plan.PasswordManager.StripeStoragePlanId _updatedSubscription.Plan.PasswordManager.StripeStoragePlanId
}; ];
/// <summary> /// <summary>
/// Generates the <see cref="SubscriptionItemOptions"/> necessary to revert an <see cref="Organization"/>'s /// Generates the <see cref="SubscriptionItemOptions"/> necessary to revert an <see cref="Organization"/>'s
@ -94,7 +97,7 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
*/ */
/// <summary> /// <summary>
/// Checks whether the updates provided in the <see cref="CompleteSubscriptionUpdate"/>'s constructor /// Checks whether the updates provided in the <see cref="CompleteSubscriptionUpdate"/>'s constructor
/// are actually different than the organization's current <see cref="Subscription"/>. /// are actually different from the organization's current <see cref="Subscription"/>.
/// </summary> /// </summary>
/// <param name="subscription">The organization's <see cref="Subscription"/>.</param> /// <param name="subscription">The organization's <see cref="Subscription"/>.</param>
public override bool UpdateNeeded(Subscription subscription) public override bool UpdateNeeded(Subscription subscription)
@ -278,11 +281,8 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
}; };
} }
private static SubscriptionData GetSubscriptionDataFor(Organization organization) private static SubscriptionData GetSubscriptionDataFor(Organization organization, Plan plan)
{ => new()
var plan = Utilities.StaticStore.GetPlan(organization.PlanType);
return new SubscriptionData
{ {
Plan = plan, Plan = plan,
PurchasedPasswordManagerSeats = organization.Seats.HasValue PurchasedPasswordManagerSeats = organization.Seats.HasValue
@ -299,5 +299,4 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
? organization.MaxStorageGb.Value - (plan.PasswordManager.BaseStorageGb ?? 0) : ? organization.MaxStorageGb.Value - (plan.PasswordManager.BaseStorageGb ?? 0) :
0 0
}; };
}
} }

View File

@ -2,6 +2,7 @@
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Stripe; using Stripe;
using Plan = Bit.Core.Models.StaticStore.Plan;
namespace Bit.Core.Models.Business; namespace Bit.Core.Models.Business;
@ -14,18 +15,16 @@ public class ProviderSubscriptionUpdate : SubscriptionUpdate
protected override List<string> PlanIds => [_planId]; protected override List<string> PlanIds => [_planId];
public ProviderSubscriptionUpdate( public ProviderSubscriptionUpdate(
PlanType planType, Plan plan,
int previouslyPurchasedSeats, int previouslyPurchasedSeats,
int newlyPurchasedSeats) int newlyPurchasedSeats)
{ {
if (!planType.SupportsConsolidatedBilling()) if (!plan.Type.SupportsConsolidatedBilling())
{ {
throw new BillingException( throw new BillingException(
message: $"Cannot create a {nameof(ProviderSubscriptionUpdate)} for {nameof(PlanType)} that doesn't support consolidated billing"); message: $"Cannot create a {nameof(ProviderSubscriptionUpdate)} for {nameof(PlanType)} that doesn't support consolidated billing");
} }
var plan = Utilities.StaticStore.GetPlan(planType);
_planId = plan.PasswordManager.StripeProviderPortalSeatPlanId; _planId = plan.PasswordManager.StripeProviderPortalSeatPlanId;
_previouslyPurchasedSeats = previouslyPurchasedSeats; _previouslyPurchasedSeats = previouslyPurchasedSeats;
_newlyPurchasedSeats = newlyPurchasedSeats; _newlyPurchasedSeats = newlyPurchasedSeats;

View File

@ -7,6 +7,7 @@ namespace Bit.Core.Models.Business;
public class SecretsManagerSubscriptionUpdate public class SecretsManagerSubscriptionUpdate
{ {
public Organization Organization { get; } public Organization Organization { get; }
public Plan Plan { get; }
/// <summary> /// <summary>
/// The total seats the organization will have after the update, including any base seats included in the plan /// The total seats the organization will have after the update, including any base seats included in the plan
@ -49,21 +50,16 @@ public class SecretsManagerSubscriptionUpdate
public bool MaxAutoscaleSmSeatsChanged => MaxAutoscaleSmSeats != Organization.MaxAutoscaleSmSeats; public bool MaxAutoscaleSmSeatsChanged => MaxAutoscaleSmSeats != Organization.MaxAutoscaleSmSeats;
public bool MaxAutoscaleSmServiceAccountsChanged => public bool MaxAutoscaleSmServiceAccountsChanged =>
MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts; MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts;
public Plan Plan => Utilities.StaticStore.GetPlan(Organization.PlanType);
public bool SmSeatAutoscaleLimitReached => SmSeats.HasValue && MaxAutoscaleSmSeats.HasValue && SmSeats == MaxAutoscaleSmSeats; public bool SmSeatAutoscaleLimitReached => SmSeats.HasValue && MaxAutoscaleSmSeats.HasValue && SmSeats == MaxAutoscaleSmSeats;
public bool SmServiceAccountAutoscaleLimitReached => SmServiceAccounts.HasValue && public bool SmServiceAccountAutoscaleLimitReached => SmServiceAccounts.HasValue &&
MaxAutoscaleSmServiceAccounts.HasValue && MaxAutoscaleSmServiceAccounts.HasValue &&
SmServiceAccounts == MaxAutoscaleSmServiceAccounts; SmServiceAccounts == MaxAutoscaleSmServiceAccounts;
public SecretsManagerSubscriptionUpdate(Organization organization, bool autoscaling) public SecretsManagerSubscriptionUpdate(Organization organization, Plan plan, bool autoscaling)
{ {
if (organization == null) Organization = organization ?? throw new NotFoundException("Organization is not found.");
{ Plan = plan;
throw new NotFoundException("Organization is not found.");
}
Organization = organization;
if (!Plan.SupportsSecretsManager) if (!Plan.SupportsSecretsManager)
{ {

View File

@ -82,7 +82,6 @@ public class SubscriptionInfo
} }
public bool AddonSubscriptionItem { get; set; } public bool AddonSubscriptionItem { get; set; }
public string ProductId { get; set; } public string ProductId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public decimal Amount { get; set; } public decimal Amount { get; set; }

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -54,8 +55,9 @@ public class CloudSyncSponsorshipsCommand : ICloudSyncSponsorshipsCommand
foreach (var selfHostedSponsorship in sponsorshipsData) foreach (var selfHostedSponsorship in sponsorshipsData)
{ {
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(selfHostedSponsorship.PlanSponsorshipType)?.SponsoringProductTierType; var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(selfHostedSponsorship.PlanSponsorshipType)?.SponsoringProductTierType;
var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier();
if (requiredSponsoringProductType == null if (requiredSponsoringProductType == null
|| StaticStore.GetPlan(sponsoringOrg.PlanType).ProductTier != requiredSponsoringProductType.Value) || sponsoringOrgProductTier != requiredSponsoringProductType.Value)
{ {
continue; // prevent unsupported sponsorships continue; // prevent unsupported sponsorships
} }

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
@ -50,9 +51,10 @@ public class SetUpSponsorshipCommand : ISetUpSponsorshipCommand
// Check org to sponsor's product type // Check org to sponsor's product type
var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductTierType; var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductTierType;
var sponsoredOrganizationProductTier = sponsoredOrganization.PlanType.GetProductTier();
if (requiredSponsoredProductType == null || if (requiredSponsoredProductType == null ||
sponsoredOrganization == null || sponsoredOrganizationProductTier != requiredSponsoredProductType.Value)
StaticStore.GetPlan(sponsoredOrganization.PlanType).ProductTier != requiredSponsoredProductType.Value)
{ {
throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); throw new BadRequestException("Can only redeem sponsorship offer on families organizations.");
} }

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -103,8 +104,6 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo
return false; return false;
} }
var sponsoringOrgPlan = Utilities.StaticStore.GetPlan(sponsoringOrganization.PlanType);
if (OrgDisabledForMoreThanGracePeriod(sponsoringOrganization)) if (OrgDisabledForMoreThanGracePeriod(sponsoringOrganization))
{ {
_logger.LogWarning("Sponsoring Organization {SponsoringOrganizationId} is disabled for more than 3 months.", sponsoringOrganization.Id); _logger.LogWarning("Sponsoring Organization {SponsoringOrganizationId} is disabled for more than 3 months.", sponsoringOrganization.Id);
@ -113,7 +112,9 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo
return false; return false;
} }
if (sponsoredPlan.SponsoringProductTierType != sponsoringOrgPlan.ProductTier) var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier();
if (sponsoredPlan.SponsoringProductTierType != sponsoringOrgProductTier)
{ {
_logger.LogWarning("Sponsoring Organization {SponsoringOrganizationId} is not on the required product type.", sponsoringOrganization.Id); _logger.LogWarning("Sponsoring Organization {SponsoringOrganizationId} is not on the required product type.", sponsoringOrganization.Id);
await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship); await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -31,9 +32,10 @@ public class CreateSponsorshipCommand : ICreateSponsorshipCommand
} }
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductTierType; var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductTierType;
var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier();
if (requiredSponsoringProductType == null || if (requiredSponsoringProductType == null ||
sponsoringOrg == null || sponsoringOrgProductTier != requiredSponsoringProductType.Value)
StaticStore.GetPlan(sponsoringOrg.PlanType).ProductTier != requiredSponsoringProductType.Value)
{ {
throw new BadRequestException("Specified Organization cannot sponsor other organizations."); throw new BadRequestException("Specified Organization cannot sponsor other organizations.");
} }

View File

@ -2,11 +2,11 @@
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions; namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
@ -15,22 +15,25 @@ public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscripti
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IProviderRepository _providerRepository; private readonly IProviderRepository _providerRepository;
private readonly IPricingClient _pricingClient;
public AddSecretsManagerSubscriptionCommand( public AddSecretsManagerSubscriptionCommand(
IPaymentService paymentService, IPaymentService paymentService,
IOrganizationService organizationService, IOrganizationService organizationService,
IProviderRepository providerRepository) IProviderRepository providerRepository,
IPricingClient pricingClient)
{ {
_paymentService = paymentService; _paymentService = paymentService;
_organizationService = organizationService; _organizationService = organizationService;
_providerRepository = providerRepository; _providerRepository = providerRepository;
_pricingClient = pricingClient;
} }
public async Task SignUpAsync(Organization organization, int additionalSmSeats, public async Task SignUpAsync(Organization organization, int additionalSmSeats,
int additionalServiceAccounts) int additionalServiceAccounts)
{ {
await ValidateOrganization(organization); await ValidateOrganization(organization);
var plan = StaticStore.GetPlan(organization.PlanType); var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var signup = SetOrganizationUpgrade(organization, additionalSmSeats, additionalServiceAccounts); var signup = SetOrganizationUpgrade(organization, additionalSmSeats, additionalServiceAccounts);
_organizationService.ValidateSecretsManagerPlan(plan, signup); _organizationService.ValidateSecretsManagerPlan(plan, signup);
@ -73,7 +76,13 @@ public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscripti
throw new BadRequestException("Organization already uses Secrets Manager."); throw new BadRequestException("Organization already uses Secrets Manager.");
} }
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType && p.SupportsSecretsManager); var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (!plan.SupportsSecretsManager)
{
throw new BadRequestException("Organization's plan does not support Secrets Manager.");
}
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) && plan.ProductTier != ProductTierType.Free) if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) && plan.ProductTier != ProductTierType.Free)
{ {
throw new BadRequestException("No payment method found."); throw new BadRequestException("No payment method found.");

View File

@ -6,6 +6,7 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -18,7 +19,6 @@ using Bit.Core.Services;
using Bit.Core.Tools.Enums; using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services; using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions; namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
@ -38,6 +38,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IOrganizationBillingService _organizationBillingService; private readonly IOrganizationBillingService _organizationBillingService;
private readonly IPricingClient _pricingClient;
public UpgradeOrganizationPlanCommand( public UpgradeOrganizationPlanCommand(
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
@ -53,7 +54,8 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationService organizationService, IOrganizationService organizationService,
IFeatureService featureService, IFeatureService featureService,
IOrganizationBillingService organizationBillingService) IOrganizationBillingService organizationBillingService,
IPricingClient pricingClient)
{ {
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_collectionRepository = collectionRepository; _collectionRepository = collectionRepository;
@ -69,6 +71,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
_organizationService = organizationService; _organizationService = organizationService;
_featureService = featureService; _featureService = featureService;
_organizationBillingService = organizationBillingService; _organizationBillingService = organizationBillingService;
_pricingClient = pricingClient;
} }
public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade) public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
@ -84,14 +87,11 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
throw new BadRequestException("Your account has no payment method available."); throw new BadRequestException("Your account has no payment method available.");
} }
var existingPlan = StaticStore.GetPlan(organization.PlanType); var existingPlan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (existingPlan == null)
{
throw new BadRequestException("Existing plan not found.");
}
var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled); var newPlan = await _pricingClient.GetPlanOrThrow(upgrade.Plan);
if (newPlan == null)
if (newPlan.Disabled)
{ {
throw new BadRequestException("Plan not found."); throw new BadRequestException("Plan not found.");
} }

View File

@ -7,6 +7,7 @@ using Bit.Core.Billing.Models.Api.Requests.Accounts;
using Bit.Core.Billing.Models.Api.Requests.Organizations; using Bit.Core.Billing.Models.Api.Requests.Organizations;
using Bit.Core.Billing.Models.Api.Responses; using Bit.Core.Billing.Models.Api.Responses;
using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -37,6 +38,7 @@ public class StripePaymentService : IPaymentService
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly ITaxService _taxService; private readonly ITaxService _taxService;
private readonly ISubscriberService _subscriberService; private readonly ISubscriberService _subscriberService;
private readonly IPricingClient _pricingClient;
public StripePaymentService( public StripePaymentService(
ITransactionRepository transactionRepository, ITransactionRepository transactionRepository,
@ -46,7 +48,8 @@ public class StripePaymentService : IPaymentService
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
IFeatureService featureService, IFeatureService featureService,
ITaxService taxService, ITaxService taxService,
ISubscriberService subscriberService) ISubscriberService subscriberService,
IPricingClient pricingClient)
{ {
_transactionRepository = transactionRepository; _transactionRepository = transactionRepository;
_logger = logger; _logger = logger;
@ -56,6 +59,7 @@ public class StripePaymentService : IPaymentService
_featureService = featureService; _featureService = featureService;
_taxService = taxService; _taxService = taxService;
_subscriberService = subscriberService; _subscriberService = subscriberService;
_pricingClient = pricingClient;
} }
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
@ -297,7 +301,7 @@ public class StripePaymentService : IPaymentService
OrganizationSponsorship sponsorship, OrganizationSponsorship sponsorship,
bool applySponsorship) bool applySponsorship)
{ {
var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType); var existingPlan = await _pricingClient.GetPlanOrThrow(org.PlanType);
var sponsoredPlan = sponsorship?.PlanSponsorshipType != null ? var sponsoredPlan = sponsorship?.PlanSponsorshipType != null ?
Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) : Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) :
null; null;
@ -887,18 +891,21 @@ public class StripePaymentService : IPaymentService
return paymentIntentClientSecret; return paymentIntentClientSecret;
} }
public Task<string> AdjustSubscription( public async Task<string> AdjustSubscription(
Organization organization, Organization organization,
StaticStore.Plan updatedPlan, StaticStore.Plan updatedPlan,
int newlyPurchasedPasswordManagerSeats, int newlyPurchasedPasswordManagerSeats,
bool subscribedToSecretsManager, bool subscribedToSecretsManager,
int? newlyPurchasedSecretsManagerSeats, int? newlyPurchasedSecretsManagerSeats,
int? newlyPurchasedAdditionalSecretsManagerServiceAccounts, int? newlyPurchasedAdditionalSecretsManagerServiceAccounts,
int newlyPurchasedAdditionalStorage) => int newlyPurchasedAdditionalStorage)
FinalizeSubscriptionChangeAsync( {
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
return await FinalizeSubscriptionChangeAsync(
organization, organization,
new CompleteSubscriptionUpdate( new CompleteSubscriptionUpdate(
organization, organization,
plan,
new SubscriptionData new SubscriptionData
{ {
Plan = updatedPlan, Plan = updatedPlan,
@ -909,6 +916,7 @@ public class StripePaymentService : IPaymentService
newlyPurchasedAdditionalSecretsManagerServiceAccounts, newlyPurchasedAdditionalSecretsManagerServiceAccounts,
PurchasedAdditionalStorage = newlyPurchasedAdditionalStorage PurchasedAdditionalStorage = newlyPurchasedAdditionalStorage
}), true); }), true);
}
public Task<string> AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats) => public Task<string> AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats) =>
FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats)); FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats));
@ -921,7 +929,7 @@ public class StripePaymentService : IPaymentService
=> FinalizeSubscriptionChangeAsync( => FinalizeSubscriptionChangeAsync(
provider, provider,
new ProviderSubscriptionUpdate( new ProviderSubscriptionUpdate(
plan.Type, plan,
currentlySubscribedSeats, currentlySubscribedSeats,
newlySubscribedSeats)); newlySubscribedSeats));
@ -1957,7 +1965,7 @@ public class StripePaymentService : IPaymentService
string gatewayCustomerId, string gatewayCustomerId,
string gatewaySubscriptionId) string gatewaySubscriptionId)
{ {
var plan = Utilities.StaticStore.GetPlan(parameters.PasswordManager.Plan); var plan = await _pricingClient.GetPlanOrThrow(parameters.PasswordManager.Plan);
var options = new InvoiceCreatePreviewOptions var options = new InvoiceCreatePreviewOptions
{ {

View File

@ -1,5 +1,6 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -137,6 +138,7 @@ public static class StaticStore
} }
public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; } public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; }
[Obsolete("Use PricingClient.ListPlans to retrieve all plans.")]
public static IEnumerable<Plan> Plans { get; } public static IEnumerable<Plan> Plans { get; }
public static IEnumerable<SponsoredPlan> SponsoredPlans { get; set; } = new[] public static IEnumerable<SponsoredPlan> SponsoredPlans { get; set; } = new[]
{ {
@ -147,10 +149,11 @@ public static class StaticStore
SponsoringProductTierType = ProductTierType.Enterprise, SponsoringProductTierType = ProductTierType.Enterprise,
StripePlanId = "2021-family-for-enterprise-annually", StripePlanId = "2021-family-for-enterprise-annually",
UsersCanSponsor = (OrganizationUserOrganizationDetails org) => UsersCanSponsor = (OrganizationUserOrganizationDetails org) =>
GetPlan(org.PlanType).ProductTier == ProductTierType.Enterprise, org.PlanType.GetProductTier() == ProductTierType.Enterprise,
} }
}; };
[Obsolete("Use PricingClient.GetPlan to retrieve a plan.")]
public static Plan GetPlan(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType); public static Plan GetPlan(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType);
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) => public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>
@ -167,6 +170,7 @@ public static class StaticStore
/// </returns> /// </returns>
public static bool IsAddonSubscriptionItem(string stripePlanId) public static bool IsAddonSubscriptionItem(string stripePlanId)
{ {
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-16844
return Plans.Any(p => return Plans.Any(p =>
p.PasswordManager.StripeStoragePlanId == stripePlanId || p.PasswordManager.StripeStoragePlanId == stripePlanId ||
(p.SecretsManager?.StripeServiceAccountPlanId == stripePlanId)); (p.SecretsManager?.StripeServiceAccountPlanId == stripePlanId));

View File

@ -17,6 +17,7 @@ using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services; using Bit.Core.Auth.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
@ -54,6 +55,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly IOrganizationDeleteCommand _organizationDeleteCommand; private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
private readonly IPricingClient _pricingClient;
private readonly OrganizationsController _sut; private readonly OrganizationsController _sut;
public OrganizationsControllerTests() public OrganizationsControllerTests()
@ -78,6 +80,7 @@ public class OrganizationsControllerTests : IDisposable
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>(); _removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
_cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>(); _cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>();
_organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>(); _organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>();
_pricingClient = Substitute.For<IPricingClient>();
_sut = new OrganizationsController( _sut = new OrganizationsController(
_organizationRepository, _organizationRepository,
@ -99,7 +102,8 @@ public class OrganizationsControllerTests : IDisposable
_orgDeleteTokenDataFactory, _orgDeleteTokenDataFactory,
_removeOrganizationUserCommand, _removeOrganizationUserCommand,
_cloudOrganizationSignUpCommand, _cloudOrganizationSignUpCommand,
_organizationDeleteCommand); _organizationDeleteCommand,
_pricingClient);
} }
public void Dispose() public void Dispose()

View File

@ -10,6 +10,7 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services; using Bit.Core.Auth.Services;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
@ -49,6 +50,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly ISubscriberService _subscriberService; private readonly ISubscriberService _subscriberService;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IOrganizationInstallationRepository _organizationInstallationRepository; private readonly IOrganizationInstallationRepository _organizationInstallationRepository;
private readonly IPricingClient _pricingClient;
private readonly OrganizationsController _sut; private readonly OrganizationsController _sut;
@ -73,6 +75,7 @@ public class OrganizationsControllerTests : IDisposable
_subscriberService = Substitute.For<ISubscriberService>(); _subscriberService = Substitute.For<ISubscriberService>();
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>(); _removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
_organizationInstallationRepository = Substitute.For<IOrganizationInstallationRepository>(); _organizationInstallationRepository = Substitute.For<IOrganizationInstallationRepository>();
_pricingClient = Substitute.For<IPricingClient>();
_sut = new OrganizationsController( _sut = new OrganizationsController(
_organizationRepository, _organizationRepository,
@ -89,7 +92,8 @@ public class OrganizationsControllerTests : IDisposable
_addSecretsManagerSubscriptionCommand, _addSecretsManagerSubscriptionCommand,
_referenceEventService, _referenceEventService,
_subscriberService, _subscriberService,
_organizationInstallationRepository); _organizationInstallationRepository,
_pricingClient);
} }
public void Dispose() public void Dispose()

View File

@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
@ -331,6 +332,11 @@ public class ProviderBillingControllerTests
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans); sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
foreach (var providerPlan in providerPlans)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(StaticStore.GetPlan(providerPlan.PlanType));
}
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
Assert.IsType<Ok<ProviderSubscriptionResponse>>(result); Assert.IsType<Ok<ProviderSubscriptionResponse>>(result);

View File

@ -2,6 +2,7 @@
using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Controllers;
using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Request;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -15,6 +16,7 @@ using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers; using Bit.Test.Common.Helpers;
@ -119,6 +121,8 @@ public class ServiceAccountsControllerTests
{ {
ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization); ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
await sutProvider.Sut.CreateAsync(organization.Id, data); await sutProvider.Sut.CreateAsync(organization.Id, data);
await sutProvider.GetDependency<ICreateServiceAccountCommand>().Received(1) await sutProvider.GetDependency<ICreateServiceAccountCommand>().Received(1)

View File

@ -1,14 +1,16 @@
using Bit.Billing.Services; using Bit.Billing.Services;
using Bit.Billing.Services.Implementations; using Bit.Billing.Services.Implementations;
using Bit.Billing.Test.Utilities; using Bit.Billing.Test.Utilities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using Stripe; using Stripe;
using Xunit; using Xunit;
@ -17,6 +19,12 @@ namespace Bit.Billing.Test.Services;
public class ProviderEventServiceTests public class ProviderEventServiceTests
{ {
private readonly IOrganizationRepository _organizationRepository =
Substitute.For<IOrganizationRepository>();
private readonly IPricingClient _pricingClient =
Substitute.For<IPricingClient>();
private readonly IProviderInvoiceItemRepository _providerInvoiceItemRepository = private readonly IProviderInvoiceItemRepository _providerInvoiceItemRepository =
Substitute.For<IProviderInvoiceItemRepository>(); Substitute.For<IProviderInvoiceItemRepository>();
@ -37,7 +45,8 @@ public class ProviderEventServiceTests
public ProviderEventServiceTests() public ProviderEventServiceTests()
{ {
_providerEventService = new ProviderEventService( _providerEventService = new ProviderEventService(
Substitute.For<ILogger<ProviderEventService>>(), _organizationRepository,
_pricingClient,
_providerInvoiceItemRepository, _providerInvoiceItemRepository,
_providerOrganizationRepository, _providerOrganizationRepository,
_providerPlanRepository, _providerPlanRepository,
@ -147,6 +156,12 @@ public class ProviderEventServiceTests
_providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId).Returns(clients); _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId).Returns(clients);
_organizationRepository.GetByIdAsync(client1Id)
.Returns(new Organization { PlanType = PlanType.TeamsMonthly });
_organizationRepository.GetByIdAsync(client2Id)
.Returns(new Organization { PlanType = PlanType.EnterpriseMonthly });
var providerPlans = new List<ProviderPlan> var providerPlans = new List<ProviderPlan>
{ {
new () new ()
@ -169,6 +184,11 @@ public class ProviderEventServiceTests
} }
}; };
foreach (var providerPlan in providerPlans)
{
_pricingClient.GetPlanOrThrow(providerPlan.PlanType).Returns(StaticStore.GetPlan(providerPlan.PlanType));
}
_providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans); _providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans);
// Act // Act

View File

@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -38,6 +39,8 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.IsFromSecretsManagerTrial = false; signup.IsFromSecretsManagerTrial = false;
signup.IsFromProvider = false; signup.IsFromProvider = false;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync( await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(
@ -66,7 +69,7 @@ public class CloudICloudOrganizationSignUpCommandTests
sale.CustomerSetup.TokenizedPaymentSource.Token == signup.PaymentToken && sale.CustomerSetup.TokenizedPaymentSource.Token == signup.PaymentToken &&
sale.CustomerSetup.TaxInformation.Country == signup.TaxInfo.BillingAddressCountry && sale.CustomerSetup.TaxInformation.Country == signup.TaxInfo.BillingAddressCountry &&
sale.CustomerSetup.TaxInformation.PostalCode == signup.TaxInfo.BillingAddressPostalCode && sale.CustomerSetup.TaxInformation.PostalCode == signup.TaxInfo.BillingAddressPostalCode &&
sale.SubscriptionSetup.Plan == plan && sale.SubscriptionSetup.PlanType == plan.Type &&
sale.SubscriptionSetup.PasswordManagerOptions.Seats == signup.AdditionalSeats && sale.SubscriptionSetup.PasswordManagerOptions.Seats == signup.AdditionalSeats &&
sale.SubscriptionSetup.PasswordManagerOptions.Storage == signup.AdditionalStorageGb && sale.SubscriptionSetup.PasswordManagerOptions.Storage == signup.AdditionalStorageGb &&
sale.SubscriptionSetup.SecretsManagerOptions == null)); sale.SubscriptionSetup.SecretsManagerOptions == null));
@ -84,6 +87,8 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.UseSecretsManager = false; signup.UseSecretsManager = false;
signup.IsFromProvider = false; signup.IsFromProvider = false;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
// Extract orgUserId when created // Extract orgUserId when created
Guid? orgUserId = null; Guid? orgUserId = null;
await sutProvider.GetDependency<IOrganizationUserRepository>() await sutProvider.GetDependency<IOrganizationUserRepository>()
@ -128,6 +133,7 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.IsFromSecretsManagerTrial = false; signup.IsFromSecretsManagerTrial = false;
signup.IsFromProvider = false; signup.IsFromProvider = false;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);
@ -157,7 +163,7 @@ public class CloudICloudOrganizationSignUpCommandTests
sale.CustomerSetup.TokenizedPaymentSource.Token == signup.PaymentToken && sale.CustomerSetup.TokenizedPaymentSource.Token == signup.PaymentToken &&
sale.CustomerSetup.TaxInformation.Country == signup.TaxInfo.BillingAddressCountry && sale.CustomerSetup.TaxInformation.Country == signup.TaxInfo.BillingAddressCountry &&
sale.CustomerSetup.TaxInformation.PostalCode == signup.TaxInfo.BillingAddressPostalCode && sale.CustomerSetup.TaxInformation.PostalCode == signup.TaxInfo.BillingAddressPostalCode &&
sale.SubscriptionSetup.Plan == plan && sale.SubscriptionSetup.PlanType == plan.Type &&
sale.SubscriptionSetup.PasswordManagerOptions.Seats == signup.AdditionalSeats && sale.SubscriptionSetup.PasswordManagerOptions.Seats == signup.AdditionalSeats &&
sale.SubscriptionSetup.PasswordManagerOptions.Storage == signup.AdditionalStorageGb && sale.SubscriptionSetup.PasswordManagerOptions.Storage == signup.AdditionalStorageGb &&
sale.SubscriptionSetup.SecretsManagerOptions.Seats == signup.AdditionalSmSeats && sale.SubscriptionSetup.SecretsManagerOptions.Seats == signup.AdditionalSmSeats &&
@ -177,6 +183,8 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.PremiumAccessAddon = false; signup.PremiumAccessAddon = false;
signup.IsFromProvider = true; signup.IsFromProvider = true;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SignUpOrganizationAsync(signup)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message); Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message);
} }
@ -195,6 +203,8 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.AdditionalStorageGb = 0; signup.AdditionalStorageGb = 0;
signup.IsFromProvider = false; signup.IsFromProvider = false;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup)); () => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message); Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message);
@ -213,6 +223,8 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.AdditionalServiceAccounts = 10; signup.AdditionalServiceAccounts = 10;
signup.IsFromProvider = false; signup.IsFromProvider = false;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup)); () => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message); Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message);
@ -231,6 +243,8 @@ public class CloudICloudOrganizationSignUpCommandTests
signup.AdditionalServiceAccounts = -10; signup.AdditionalServiceAccounts = -10;
signup.IsFromProvider = false; signup.IsFromProvider = false;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpOrganizationAsync(signup)); () => sutProvider.Sut.SignUpOrganizationAsync(signup));
Assert.Contains("You can't subtract Machine Accounts!", exception.Message); Assert.Contains("You can't subtract Machine Accounts!", exception.Message);
@ -249,6 +263,8 @@ public class CloudICloudOrganizationSignUpCommandTests
Owner = new User { Id = Guid.NewGuid() } Owner = new User { Id = Guid.NewGuid() }
}; };
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan));
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id) .GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id)
.Returns(1); .Returns(1);

View File

@ -10,6 +10,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -203,10 +204,12 @@ public class OrganizationServiceTests
{ {
signup.Plan = PlanType.TeamsMonthly; signup.Plan = PlanType.TeamsMonthly;
var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup);
var plan = StaticStore.GetPlan(signup.Plan); var plan = StaticStore.GetPlan(signup.Plan);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(plan);
var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(Arg.Is<Organization>(org => await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(Arg.Is<Organization>(org =>
org.Id == organization.Id && org.Id == organization.Id &&
org.Name == signup.Name && org.Name == signup.Name &&
@ -894,6 +897,8 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites); await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites);
await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().Received(1) await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().Received(1)
@ -933,6 +938,9 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
sutProvider.GetDependency<IReferenceEventService>().RaiseEventAsync(default) sutProvider.GetDependency<IReferenceEventService>().RaiseEventAsync(default)
.ThrowsForAnyArgs<BadRequestException>(); .ThrowsForAnyArgs<BadRequestException>();
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
await Assert.ThrowsAsync<AggregateException>(async () => await Assert.ThrowsAsync<AggregateException>(async () =>
await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites)); await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites));
@ -1338,6 +1346,9 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
organization.MaxAutoscaleSeats = currentMaxAutoscaleSeats; organization.MaxAutoscaleSeats = currentMaxAutoscaleSeats;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscription(organization.Id, var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscription(organization.Id,
seatAdjustment, maxAutoscaleSeats)); seatAdjustment, maxAutoscaleSeats));
@ -1360,6 +1371,9 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
organization.Seats = 100; organization.Seats = 100;
organization.SmSeats = 100; organization.SmSeats = 100;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, null)); var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, null));

View File

@ -1,8 +1,10 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations; using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
@ -15,43 +17,6 @@ namespace Bit.Core.Test.Billing.Services;
public class OrganizationBillingServiceTests public class OrganizationBillingServiceTests
{ {
#region GetMetadata #region GetMetadata
[Theory, BitAutoData]
public async Task GetMetadata_OrganizationNull_ReturnsNull(
Guid organizationId,
SutProvider<OrganizationBillingService> sutProvider)
{
var metadata = await sutProvider.Sut.GetMetadata(organizationId);
Assert.Null(metadata);
}
[Theory, BitAutoData]
public async Task GetMetadata_CustomerNull_ReturnsNull(
Guid organizationId,
Organization organization,
SutProvider<OrganizationBillingService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
var metadata = await sutProvider.Sut.GetMetadata(organizationId);
Assert.False(metadata.IsOnSecretsManagerStandalone);
}
[Theory, BitAutoData]
public async Task GetMetadata_SubscriptionNull_ReturnsNull(
Guid organizationId,
Organization organization,
SutProvider<OrganizationBillingService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
sutProvider.GetDependency<ISubscriberService>().GetCustomer(organization).Returns(new Customer());
var metadata = await sutProvider.Sut.GetMetadata(organizationId);
Assert.False(metadata.IsOnSecretsManagerStandalone);
}
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetMetadata_Succeeds( public async Task GetMetadata_Succeeds(
@ -61,6 +26,11 @@ public class OrganizationBillingServiceTests
{ {
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
sutProvider.GetDependency<IPricingClient>().ListPlans().Returns(StaticStore.Plans.ToList());
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var subscriberService = sutProvider.GetDependency<ISubscriberService>(); var subscriberService = sutProvider.GetDependency<ISubscriberService>();
subscriberService subscriberService
@ -99,7 +69,8 @@ public class OrganizationBillingServiceTests
var metadata = await sutProvider.Sut.GetMetadata(organizationId); var metadata = await sutProvider.Sut.GetMetadata(organizationId);
Assert.True(metadata.IsOnSecretsManagerStandalone); Assert.True(metadata!.IsOnSecretsManagerStandalone);
} }
#endregion #endregion
} }

View File

@ -43,7 +43,7 @@ public class CompleteSubscriptionUpdateTests
PurchasedPasswordManagerSeats = 20 PurchasedPasswordManagerSeats = 20
}; };
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsStarterPlan, updatedSubscriptionData);
var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription); var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);
@ -114,7 +114,7 @@ public class CompleteSubscriptionUpdateTests
PurchasedAdditionalStorage = 10 PurchasedAdditionalStorage = 10
}; };
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData);
var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription); var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);
@ -221,7 +221,7 @@ public class CompleteSubscriptionUpdateTests
PurchasedAdditionalStorage = 10 PurchasedAdditionalStorage = 10
}; };
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData);
var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription); var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);
@ -302,7 +302,7 @@ public class CompleteSubscriptionUpdateTests
PurchasedPasswordManagerSeats = 20 PurchasedPasswordManagerSeats = 20
}; };
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsStarterPlan, updatedSubscriptionData);
var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription); var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);
@ -372,7 +372,7 @@ public class CompleteSubscriptionUpdateTests
PurchasedAdditionalStorage = 10 PurchasedAdditionalStorage = 10
}; };
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData);
var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription); var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);
@ -478,7 +478,7 @@ public class CompleteSubscriptionUpdateTests
PurchasedAdditionalStorage = 10 PurchasedAdditionalStorage = 10
}; };
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData);
var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription); var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);

View File

@ -2,7 +2,9 @@
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Xunit; using Xunit;
@ -11,19 +13,40 @@ namespace Bit.Core.Test.Models.Business;
[SecretsManagerOrganizationCustomize] [SecretsManagerOrganizationCustomize]
public class SecretsManagerSubscriptionUpdateTests public class SecretsManagerSubscriptionUpdateTests
{ {
private static TheoryData<Plan> ToPlanTheory(List<PlanType> types)
{
var theoryData = new TheoryData<Plan>();
var plans = types.Select(StaticStore.GetPlan).ToArray();
theoryData.AddRange(plans);
return theoryData;
}
public static TheoryData<Plan> NonSmPlans =>
ToPlanTheory([PlanType.Custom, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019]);
public static TheoryData<Plan> SmPlans => ToPlanTheory([
PlanType.EnterpriseAnnually2019,
PlanType.EnterpriseAnnually,
PlanType.TeamsMonthly2019,
PlanType.TeamsAnnually2020,
PlanType.TeamsMonthly,
PlanType.TeamsAnnually2019,
PlanType.TeamsAnnually2020,
PlanType.TeamsAnnually,
PlanType.TeamsStarter
]);
[Theory] [Theory]
[BitAutoData(PlanType.Custom)] [BitMemberAutoData(nameof(NonSmPlans))]
[BitAutoData(PlanType.FamiliesAnnually)]
[BitAutoData(PlanType.FamiliesAnnually2019)]
public Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException( public Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
PlanType planType, Plan plan,
Organization organization) Organization organization)
{ {
// Arrange // Arrange
organization.PlanType = planType; organization.PlanType = plan.Type;
// Act // Act
var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, false)); var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, plan, false));
// Assert // Assert
Assert.Contains("Invalid Secrets Manager plan", exception.Message, StringComparison.InvariantCultureIgnoreCase); Assert.Contains("Invalid Secrets Manager plan", exception.Message, StringComparison.InvariantCultureIgnoreCase);
@ -31,28 +54,16 @@ public class SecretsManagerSubscriptionUpdateTests
} }
[Theory] [Theory]
[BitAutoData(PlanType.EnterpriseMonthly2019)] [BitMemberAutoData(nameof(SmPlans))]
[BitAutoData(PlanType.EnterpriseMonthly2020)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.TeamsAnnually2020)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsStarter)]
public void UpdateSubscription_WithNonSecretsManagerPlanType_DoesNotThrowException( public void UpdateSubscription_WithNonSecretsManagerPlanType_DoesNotThrowException(
PlanType planType, Plan plan,
Organization organization) Organization organization)
{ {
// Arrange // Arrange
organization.PlanType = planType; organization.PlanType = plan.Type;
// Act // Act
var ex = Record.Exception(() => new SecretsManagerSubscriptionUpdate(organization, false)); var ex = Record.Exception(() => new SecretsManagerSubscriptionUpdate(organization, plan, false));
// Assert // Assert
Assert.Null(ex); Assert.Null(ex);

View File

@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore; using Bit.Core.Models.StaticStore;
@ -41,7 +42,8 @@ public class AddSecretsManagerSubscriptionCommandTests
{ {
organization.PlanType = planType; organization.PlanType = planType;
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); var plan = StaticStore.GetPlan(organization.PlanType);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(plan);
await sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts); await sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts);
@ -85,6 +87,8 @@ public class AddSecretsManagerSubscriptionCommandTests
{ {
organization.GatewayCustomerId = null; organization.GatewayCustomerId = null;
organization.PlanType = PlanType.EnterpriseAnnually; organization.PlanType = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts)); sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts));
Assert.Contains("No payment method found.", exception.Message); Assert.Contains("No payment method found.", exception.Message);
@ -101,6 +105,8 @@ public class AddSecretsManagerSubscriptionCommandTests
{ {
organization.GatewaySubscriptionId = null; organization.GatewaySubscriptionId = null;
organization.PlanType = PlanType.EnterpriseAnnually; organization.PlanType = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts)); sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts));
Assert.Contains("No subscription found.", exception.Message); Assert.Contains("No subscription found.", exception.Message);
@ -132,6 +138,8 @@ public class AddSecretsManagerSubscriptionCommandTests
organization.UseSecretsManager = false; organization.UseSecretsManager = false;
provider.Type = ProviderType.Msp; provider.Type = ProviderType.Msp;
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider); sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(organization, 10, 10)); () => sutProvider.Sut.SignUpAsync(organization, 10, 10));

View File

@ -21,26 +21,48 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationSubscriptionUpdate;
[SecretsManagerOrganizationCustomize] [SecretsManagerOrganizationCustomize]
public class UpdateSecretsManagerSubscriptionCommandTests public class UpdateSecretsManagerSubscriptionCommandTests
{ {
private static TheoryData<Plan> ToPlanTheory(List<PlanType> types)
{
var theoryData = new TheoryData<Plan>();
var plans = types.Select(StaticStore.GetPlan).ToArray();
theoryData.AddRange(plans);
return theoryData;
}
public static TheoryData<Plan> AllTeamsAndEnterprise
=> ToPlanTheory([
PlanType.EnterpriseAnnually2019,
PlanType.EnterpriseAnnually2020,
PlanType.EnterpriseAnnually,
PlanType.EnterpriseMonthly2019,
PlanType.EnterpriseMonthly2020,
PlanType.EnterpriseMonthly,
PlanType.TeamsMonthly2019,
PlanType.TeamsMonthly2020,
PlanType.TeamsMonthly,
PlanType.TeamsAnnually2019,
PlanType.TeamsAnnually2020,
PlanType.TeamsAnnually,
PlanType.TeamsStarter
]);
public static TheoryData<Plan> CurrentTeamsAndEnterprise
=> ToPlanTheory([
PlanType.EnterpriseAnnually,
PlanType.EnterpriseMonthly,
PlanType.TeamsMonthly,
PlanType.TeamsAnnually,
PlanType.TeamsStarter
]);
[Theory] [Theory]
[BitAutoData(PlanType.EnterpriseAnnually2019)] [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseMonthly2020)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.TeamsAnnually2020)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsStarter)]
public async Task UpdateSubscriptionAsync_UpdateEverything_ValidInput_Passes( public async Task UpdateSubscriptionAsync_UpdateEverything_ValidInput_Passes(
PlanType planType, Plan plan,
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
organization.PlanType = planType; organization.PlanType = plan.Type;
organization.Seats = 400; organization.Seats = 400;
organization.SmSeats = 10; organization.SmSeats = 10;
organization.MaxAutoscaleSmSeats = 20; organization.MaxAutoscaleSmSeats = 20;
@ -52,7 +74,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var updateMaxAutoscaleSmSeats = 16; var updateMaxAutoscaleSmSeats = 16;
var updateMaxAutoscaleSmServiceAccounts = 301; var updateMaxAutoscaleSmServiceAccounts = 301;
var update = new SecretsManagerSubscriptionUpdate(organization, false) var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
SmSeats = updateSmSeats, SmSeats = updateSmSeats,
SmServiceAccounts = updateSmServiceAccounts, SmServiceAccounts = updateSmServiceAccounts,
@ -62,7 +84,6 @@ public class UpdateSecretsManagerSubscriptionCommandTests
await sutProvider.Sut.UpdateSubscriptionAsync(update); await sutProvider.Sut.UpdateSubscriptionAsync(update);
var plan = StaticStore.GetPlan(organization.PlanType);
await sutProvider.GetDependency<IPaymentService>().Received(1) await sutProvider.GetDependency<IPaymentService>().Received(1)
.AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase); .AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase);
await sutProvider.GetDependency<IPaymentService>().Received(1) await sutProvider.GetDependency<IPaymentService>().Received(1)
@ -83,17 +104,13 @@ public class UpdateSecretsManagerSubscriptionCommandTests
} }
[Theory] [Theory]
[BitAutoData(PlanType.EnterpriseAnnually)] [BitMemberAutoData(nameof(CurrentTeamsAndEnterprise))]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsStarter)]
public async Task UpdateSubscriptionAsync_ValidInput_WithNullMaxAutoscale_Passes( public async Task UpdateSubscriptionAsync_ValidInput_WithNullMaxAutoscale_Passes(
PlanType planType, Plan plan,
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
organization.PlanType = planType; organization.PlanType = plan.Type;
organization.Seats = 20; organization.Seats = 20;
const int updateSmSeats = 15; const int updateSmSeats = 15;
@ -102,7 +119,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
// Ensure that SmSeats is different from the original organization.SmSeats // Ensure that SmSeats is different from the original organization.SmSeats
organization.SmSeats = updateSmSeats + 5; organization.SmSeats = updateSmSeats + 5;
var update = new SecretsManagerSubscriptionUpdate(organization, false) var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
SmSeats = updateSmSeats, SmSeats = updateSmSeats,
MaxAutoscaleSmSeats = null, MaxAutoscaleSmSeats = null,
@ -112,7 +129,6 @@ public class UpdateSecretsManagerSubscriptionCommandTests
await sutProvider.Sut.UpdateSubscriptionAsync(update); await sutProvider.Sut.UpdateSubscriptionAsync(update);
var plan = StaticStore.GetPlan(organization.PlanType);
await sutProvider.GetDependency<IPaymentService>().Received(1) await sutProvider.GetDependency<IPaymentService>().Received(1)
.AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase); .AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase);
await sutProvider.GetDependency<IPaymentService>().Received(1) await sutProvider.GetDependency<IPaymentService>().Received(1)
@ -141,7 +157,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var update = new SecretsManagerSubscriptionUpdate(organization, autoscaling).AdjustSeats(2); var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, autoscaling).AdjustSeats(2);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true); sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
@ -156,8 +173,10 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider, SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider,
Organization organization) Organization organization)
{ {
var plan = StaticStore.GetPlan(organization.PlanType);
organization.UseSecretsManager = false; organization.UseSecretsManager = false;
var update = new SecretsManagerSubscriptionUpdate(organization, false); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false);
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateSubscriptionAsync(update)); () => sutProvider.Sut.UpdateSubscriptionAsync(update));
@ -167,27 +186,16 @@ public class UpdateSecretsManagerSubscriptionCommandTests
} }
[Theory] [Theory]
[BitAutoData(PlanType.EnterpriseAnnually2019)] [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseMonthly2020)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.TeamsAnnually2020)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsStarter)]
public async Task UpdateSubscriptionAsync_PaidPlan_NullGatewayCustomerId_ThrowsException( public async Task UpdateSubscriptionAsync_PaidPlan_NullGatewayCustomerId_ThrowsException(
PlanType planType, Plan plan,
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
organization.PlanType = planType; organization.PlanType = plan.Type;
organization.GatewayCustomerId = null; organization.GatewayCustomerId = null;
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("No payment method found.", exception.Message); Assert.Contains("No payment method found.", exception.Message);
@ -195,27 +203,15 @@ public class UpdateSecretsManagerSubscriptionCommandTests
} }
[Theory] [Theory]
[BitAutoData(PlanType.EnterpriseAnnually2019)] [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseMonthly2020)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.TeamsAnnually2020)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsStarter)]
public async Task UpdateSubscriptionAsync_PaidPlan_NullGatewaySubscriptionId_ThrowsException( public async Task UpdateSubscriptionAsync_PaidPlan_NullGatewaySubscriptionId_ThrowsException(
PlanType planType, Plan plan,
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
organization.PlanType = planType; organization.PlanType = plan.Type;
organization.GatewaySubscriptionId = null; organization.GatewaySubscriptionId = null;
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("No subscription found.", exception.Message); Assert.Contains("No subscription found.", exception.Message);
@ -223,24 +219,12 @@ public class UpdateSecretsManagerSubscriptionCommandTests
} }
[Theory] [Theory]
[BitAutoData(PlanType.EnterpriseAnnually2019)] [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]
[BitAutoData(PlanType.EnterpriseAnnually2020)] public async Task AdjustServiceAccountsAsync_WithEnterpriseOrTeamsPlans_Success(
[BitAutoData(PlanType.EnterpriseAnnually)] Plan plan,
[BitAutoData(PlanType.EnterpriseMonthly2019)] Guid organizationId,
[BitAutoData(PlanType.EnterpriseMonthly2020)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.TeamsAnnually2020)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsStarter)]
public async Task AdjustServiceAccountsAsync_WithEnterpriseOrTeamsPlans_Success(PlanType planType, Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var plan = StaticStore.GetPlan(planType);
var organizationSeats = plan.SecretsManager.BaseSeats + 10; var organizationSeats = plan.SecretsManager.BaseSeats + 10;
var organizationMaxAutoscaleSeats = 20; var organizationMaxAutoscaleSeats = 20;
var organizationServiceAccounts = plan.SecretsManager.BaseServiceAccount + 10; var organizationServiceAccounts = plan.SecretsManager.BaseServiceAccount + 10;
@ -249,7 +233,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var organization = new Organization var organization = new Organization
{ {
Id = organizationId, Id = organizationId,
PlanType = planType, PlanType = plan.Type,
GatewayCustomerId = "1", GatewayCustomerId = "1",
GatewaySubscriptionId = "2", GatewaySubscriptionId = "2",
UseSecretsManager = true, UseSecretsManager = true,
@ -263,7 +247,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var expectedSmServiceAccounts = organizationServiceAccounts + smServiceAccountsAdjustment; var expectedSmServiceAccounts = organizationServiceAccounts + smServiceAccountsAdjustment;
var expectedSmServiceAccountsExcludingBase = expectedSmServiceAccounts - plan.SecretsManager.BaseServiceAccount; var expectedSmServiceAccountsExcludingBase = expectedSmServiceAccounts - plan.SecretsManager.BaseServiceAccount;
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(10); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(10);
await sutProvider.Sut.UpdateSubscriptionAsync(update); await sutProvider.Sut.UpdateSubscriptionAsync(update);
@ -290,8 +274,9 @@ public class UpdateSecretsManagerSubscriptionCommandTests
// Make sure Password Manager seats is greater or equal to Secrets Manager seats // Make sure Password Manager seats is greater or equal to Secrets Manager seats
organization.Seats = seatCount; organization.Seats = seatCount;
var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, false) var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
SmSeats = seatCount, SmSeats = seatCount,
MaxAutoscaleSmSeats = seatCount MaxAutoscaleSmSeats = seatCount
@ -310,7 +295,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
organization.SmSeats = null; organization.SmSeats = null;
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1); var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1);
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateSubscriptionAsync(update)); () => sutProvider.Sut.UpdateSubscriptionAsync(update));
@ -325,7 +311,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustSeats(-2); var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustSeats(-2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Cannot use autoscaling to subtract seats.", exception.Message); Assert.Contains("Cannot use autoscaling to subtract seats.", exception.Message);
@ -340,7 +327,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
organization.PlanType = planType; organization.PlanType = planType;
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1); var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("You have reached the maximum number of Secrets Manager seats (2) for this plan", Assert.Contains("You have reached the maximum number of Secrets Manager seats (2) for this plan",
@ -357,7 +345,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
organization.SmSeats = 9; organization.SmSeats = 9;
organization.MaxAutoscaleSmSeats = 10; organization.MaxAutoscaleSmSeats = 10;
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustSeats(2); var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustSeats(2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Secrets Manager seat limit has been reached.", exception.Message); Assert.Contains("Secrets Manager seat limit has been reached.", exception.Message);
@ -370,7 +359,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var update = new SecretsManagerSubscriptionUpdate(organization, false) var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
SmSeats = organization.SmSeats + 10, SmSeats = organization.SmSeats + 10,
MaxAutoscaleSmSeats = organization.SmSeats + 5 MaxAutoscaleSmSeats = organization.SmSeats + 5
@ -388,7 +378,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var update = new SecretsManagerSubscriptionUpdate(organization, false) var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
SmSeats = 0, SmSeats = 0,
}; };
@ -407,7 +398,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
organization.SmSeats = 8; organization.SmSeats = 8;
var update = new SecretsManagerSubscriptionUpdate(organization, false) var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
SmSeats = 7, SmSeats = 7,
}; };
@ -425,7 +417,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var update = new SecretsManagerSubscriptionUpdate(organization, false) var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
SmServiceAccounts = 300, SmServiceAccounts = 300,
MaxAutoscaleSmServiceAccounts = 300 MaxAutoscaleSmServiceAccounts = 300
@ -444,7 +437,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
organization.SmServiceAccounts = null; organization.SmServiceAccounts = null;
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1); var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(1);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Organization has no machine accounts limit, no need to adjust machine accounts", exception.Message); Assert.Contains("Organization has no machine accounts limit, no need to adjust machine accounts", exception.Message);
@ -457,7 +451,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(-2); var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustServiceAccounts(-2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Cannot use autoscaling to subtract machine accounts.", exception.Message); Assert.Contains("Cannot use autoscaling to subtract machine accounts.", exception.Message);
@ -472,7 +467,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
organization.PlanType = planType; organization.PlanType = planType;
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1); var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(1);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("You have reached the maximum number of machine accounts (3) for this plan", Assert.Contains("You have reached the maximum number of machine accounts (3) for this plan",
@ -489,7 +485,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
organization.SmServiceAccounts = 9; organization.SmServiceAccounts = 9;
organization.MaxAutoscaleSmServiceAccounts = 10; organization.MaxAutoscaleSmServiceAccounts = 10;
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(2); var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustServiceAccounts(2);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Secrets Manager machine account limit has been reached.", exception.Message); Assert.Contains("Secrets Manager machine account limit has been reached.", exception.Message);
@ -508,7 +505,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
organization.SmServiceAccounts = smServiceAccount - 5; organization.SmServiceAccounts = smServiceAccount - 5;
organization.MaxAutoscaleSmServiceAccounts = 2 * smServiceAccount; organization.MaxAutoscaleSmServiceAccounts = 2 * smServiceAccount;
var update = new SecretsManagerSubscriptionUpdate(organization, false) var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
SmServiceAccounts = smServiceAccount, SmServiceAccounts = smServiceAccount,
MaxAutoscaleSmServiceAccounts = maxAutoscaleSmServiceAccounts MaxAutoscaleSmServiceAccounts = maxAutoscaleSmServiceAccounts
@ -530,7 +528,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
organization.SmServiceAccounts = newSmServiceAccounts - 10; organization.SmServiceAccounts = newSmServiceAccounts - 10;
var update = new SecretsManagerSubscriptionUpdate(organization, false) var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
SmServiceAccounts = newSmServiceAccounts, SmServiceAccounts = newSmServiceAccounts,
}; };
@ -542,28 +541,16 @@ public class UpdateSecretsManagerSubscriptionCommandTests
} }
[Theory] [Theory]
[BitAutoData(PlanType.EnterpriseAnnually2019)] [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseMonthly2020)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.TeamsAnnually2020)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsStarter)]
public async Task UpdateSmServiceAccounts_WhenCurrentServiceAccountsIsGreaterThanNew_ThrowsBadRequestException( public async Task UpdateSmServiceAccounts_WhenCurrentServiceAccountsIsGreaterThanNew_ThrowsBadRequestException(
PlanType planType, Plan plan,
Organization organization, Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider) SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{ {
var currentServiceAccounts = 301; var currentServiceAccounts = 301;
organization.PlanType = planType; organization.PlanType = plan.Type;
organization.SmServiceAccounts = currentServiceAccounts; organization.SmServiceAccounts = currentServiceAccounts;
var update = new SecretsManagerSubscriptionUpdate(organization, false) { SmServiceAccounts = 201 }; var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = 201 };
sutProvider.GetDependency<IServiceAccountRepository>() sutProvider.GetDependency<IServiceAccountRepository>()
.GetServiceAccountCountByOrganizationIdAsync(organization.Id) .GetServiceAccountCountByOrganizationIdAsync(organization.Id)
@ -586,7 +573,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
organization.SmSeats = smSeats - 1; organization.SmSeats = smSeats - 1;
organization.MaxAutoscaleSmSeats = smSeats * 2; organization.MaxAutoscaleSmSeats = smSeats * 2;
var update = new SecretsManagerSubscriptionUpdate(organization, false) var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
SmSeats = smSeats, SmSeats = smSeats,
MaxAutoscaleSmSeats = maxAutoscaleSmSeats MaxAutoscaleSmSeats = maxAutoscaleSmSeats
@ -606,7 +594,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
{ {
organization.PlanType = planType; organization.PlanType = planType;
organization.SmSeats = 2; organization.SmSeats = 2;
var update = new SecretsManagerSubscriptionUpdate(organization, false) var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
MaxAutoscaleSmSeats = 3 MaxAutoscaleSmSeats = 3
}; };
@ -625,7 +614,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
{ {
organization.PlanType = planType; organization.PlanType = planType;
organization.SmSeats = 2; organization.SmSeats = 2;
var update = new SecretsManagerSubscriptionUpdate(organization, false) var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{ {
MaxAutoscaleSmSeats = 2 MaxAutoscaleSmSeats = 2
}; };
@ -645,7 +635,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
organization.PlanType = planType; organization.PlanType = planType;
organization.SmServiceAccounts = 3; organization.SmServiceAccounts = 3;
var update = new SecretsManagerSubscriptionUpdate(organization, false) { MaxAutoscaleSmServiceAccounts = 3 }; var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmServiceAccounts = 3 };
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
Assert.Contains("Your plan does not allow machine accounts autoscaling.", exception.Message); Assert.Contains("Your plan does not allow machine accounts autoscaling.", exception.Message);

View File

@ -1,4 +1,5 @@
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
@ -43,6 +44,7 @@ public class UpgradeOrganizationPlanCommandTests
SutProvider<UpgradeOrganizationPlanCommand> sutProvider) SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{ {
upgrade.Plan = organization.PlanType; upgrade.Plan = organization.PlanType;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
@ -58,6 +60,7 @@ public class UpgradeOrganizationPlanCommandTests
upgrade.AdditionalSmSeats = 10; upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalServiceAccounts = 10; upgrade.AdditionalServiceAccounts = 10;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("already on this plan", exception.Message); Assert.Contains("already on this plan", exception.Message);
@ -69,9 +72,11 @@ public class UpgradeOrganizationPlanCommandTests
SutProvider<UpgradeOrganizationPlanCommand> sutProvider) SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{ {
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
upgrade.AdditionalSmSeats = 10; upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalSeats = 10; upgrade.AdditionalSeats = 10;
upgrade.Plan = PlanType.TeamsAnnually; upgrade.Plan = PlanType.TeamsAnnually;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization); await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);
} }
@ -92,6 +97,8 @@ public class UpgradeOrganizationPlanCommandTests
organization.PlanType = PlanType.FamiliesAnnually; organization.PlanType = PlanType.FamiliesAnnually;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
organizationUpgrade.AdditionalSeats = 30; organizationUpgrade.AdditionalSeats = 30;
organizationUpgrade.UseSecretsManager = true; organizationUpgrade.UseSecretsManager = true;
organizationUpgrade.AdditionalSmSeats = 20; organizationUpgrade.AdditionalSmSeats = 20;
@ -99,6 +106,8 @@ public class UpgradeOrganizationPlanCommandTests
organizationUpgrade.AdditionalStorageGb = 3; organizationUpgrade.AdditionalStorageGb = 3;
organizationUpgrade.Plan = planType; organizationUpgrade.Plan = planType;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organizationUpgrade.Plan).Returns(StaticStore.GetPlan(organizationUpgrade.Plan));
await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade); await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade);
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSubscription( await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSubscription(
organization, organization,
@ -120,7 +129,10 @@ public class UpgradeOrganizationPlanCommandTests
public async Task UpgradePlan_SM_Passes(PlanType planType, Organization organization, OrganizationUpgrade upgrade, public async Task UpgradePlan_SM_Passes(PlanType planType, Organization organization, OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider) SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{ {
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
upgrade.Plan = planType; upgrade.Plan = planType;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
var plan = StaticStore.GetPlan(upgrade.Plan); var plan = StaticStore.GetPlan(upgrade.Plan);
@ -155,8 +167,10 @@ public class UpgradeOrganizationPlanCommandTests
upgrade.AdditionalSeats = 15; upgrade.AdditionalSeats = 15;
upgrade.AdditionalSmSeats = 1; upgrade.AdditionalSmSeats = 1;
upgrade.AdditionalServiceAccounts = 0; upgrade.AdditionalServiceAccounts = 0;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
organization.SmSeats = 2; organization.SmSeats = 2;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()
@ -181,9 +195,11 @@ public class UpgradeOrganizationPlanCommandTests
upgrade.AdditionalSeats = 15; upgrade.AdditionalSeats = 15;
upgrade.AdditionalSmSeats = 1; upgrade.AdditionalSmSeats = 1;
upgrade.AdditionalServiceAccounts = 0; upgrade.AdditionalServiceAccounts = 0;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
organization.SmSeats = 1; organization.SmSeats = 1;
organization.SmServiceAccounts = currentServiceAccounts; organization.SmServiceAccounts = currentServiceAccounts;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IOrganizationUserRepository>() sutProvider.GetDependency<IOrganizationUserRepository>()