1
0
mirror of https://github.com/bitwarden/server.git synced 2025-03-01 04:01:11 +01: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.Billing.Constants;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Providers;
@ -27,6 +27,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly IProviderBillingService _providerBillingService;
private readonly ISubscriberService _subscriberService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
public RemoveOrganizationFromProviderCommand(
IEventService eventService,
@ -38,7 +39,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
IFeatureService featureService,
IProviderBillingService providerBillingService,
ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient)
{
_eventService = eventService;
_mailService = mailService;
@ -50,6 +52,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_providerBillingService = providerBillingService;
_subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
}
public async Task RemoveOrganizationFromProvider(
@ -110,7 +113,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
Email = organization.BillingEmail
});
var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager;
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var subscriptionCreateOptions = new SubscriptionCreateOptions
{
@ -124,7 +127,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
},
OffSession = true,
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);

View File

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

View File

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

View File

@ -28,6 +28,7 @@ public class MaxProjectsQuery : IMaxProjectsQuery
throw new NotFoundException();
}
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122
var plan = StaticStore.GetPlan(org.PlanType);
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.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -205,6 +206,8 @@ public class RemoveOrganizationFromProviderCommandTests
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
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.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -550,8 +551,14 @@ public class ProviderServiceTests
organization.PlanType = PlanType.EnterpriseMonthly;
organization.Plan = "Enterprise (Monthly)";
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var expectedPlanType = PlanType.EnterpriseMonthly2020;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(expectedPlanType)
.Returns(StaticStore.GetPlan(expectedPlanType));
var expectedPlanId = "2020-enterprise-org-seat-monthly";
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.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts;
@ -128,6 +129,9 @@ public class ProviderBillingServiceTests
.GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId))
.Returns(existingPlan);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(existingPlan.PlanType)
.Returns(StaticStore.GetPlan(existingPlan.PlanType));
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.ProviderSubscriptionGetAsync(
Arg.Is(provider.GatewaySubscriptionId),
@ -156,6 +160,9 @@ public class ProviderBillingServiceTests
var command =
new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(command.NewPlan)
.Returns(StaticStore.GetPlan(command.NewPlan));
// Act
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);
// 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();
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();
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();
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(
[
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(
[
new ProviderOrganizationOrganizationDetails
@ -856,6 +891,9 @@ public class ProviderBillingServiceTests
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.EnterpriseMonthly)
.Returns(StaticStore.GetPlan(PlanType.EnterpriseMonthly));
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
await sutProvider.GetDependency<IStripeAdapter>()
@ -881,6 +919,9 @@ public class ProviderBillingServiceTests
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly)
.Returns(StaticStore.GetPlan(PlanType.TeamsMonthly));
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
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)
.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)
.Returns(providerPlans);
@ -1066,6 +1119,12 @@ public class ProviderBillingServiceTests
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);
var command = new UpdateProviderSeatMinimumsCommand(
@ -1139,6 +1198,12 @@ public class ProviderBillingServiceTests
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);
var command = new UpdateProviderSeatMinimumsCommand(
@ -1212,6 +1277,12 @@ public class ProviderBillingServiceTests
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);
var command = new UpdateProviderSeatMinimumsCommand(
@ -1279,6 +1350,12 @@ public class ProviderBillingServiceTests
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);
var command = new UpdateProviderSeatMinimumsCommand(
@ -1352,6 +1429,12 @@ public class ProviderBillingServiceTests
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);
var command = new UpdateProviderSeatMinimumsCommand(

View File

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

View File

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

View File

@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -55,6 +56,7 @@ public class OrganizationUsersController : Controller
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
private readonly IFeatureService _featureService;
private readonly IPricingClient _pricingClient;
public OrganizationUsersController(
IOrganizationRepository organizationRepository,
@ -77,7 +79,8 @@ public class OrganizationUsersController : Controller
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
IFeatureService featureService)
IFeatureService featureService,
IPricingClient pricingClient)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -100,6 +103,7 @@ public class OrganizationUsersController : Controller
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
_featureService = featureService;
_pricingClient = pricingClient;
}
[HttpGet("{id}")]
@ -648,7 +652,9 @@ public class OrganizationUsersController : Controller
if (additionalSmSeatsRequired > 0)
{
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);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
}

View File

@ -22,6 +22,7 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Enums;
@ -60,6 +61,7 @@ public class OrganizationsController : Controller
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
private readonly IPricingClient _pricingClient;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@ -81,7 +83,8 @@ public class OrganizationsController : Controller
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
IOrganizationDeleteCommand organizationDeleteCommand)
IOrganizationDeleteCommand organizationDeleteCommand,
IPricingClient pricingClient)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -103,6 +106,7 @@ public class OrganizationsController : Controller
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
_organizationDeleteCommand = organizationDeleteCommand;
_pricingClient = pricingClient;
}
[HttpGet("{id}")]
@ -120,7 +124,8 @@ public class OrganizationsController : Controller
throw new NotFoundException();
}
return new OrganizationResponseModel(organization);
var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
}
[HttpGet("")]
@ -181,7 +186,8 @@ public class OrganizationsController : Controller
var organizationSignup = model.ToOrganizationSignup(user);
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")]
@ -196,7 +202,8 @@ public class OrganizationsController : Controller
var organizationSignup = model.ToOrganizationSignup(user);
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}")]
@ -224,7 +231,8 @@ public class OrganizationsController : Controller
}
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")]
@ -358,8 +366,8 @@ public class OrganizationsController : Controller
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
var plan = StaticStore.GetPlan(organization.PlanType);
if (plan.ProductTier is not ProductTierType.Enterprise and not ProductTierType.Teams)
var productTier = organization.PlanType.GetProductTier();
if (productTier is not ProductTierType.Enterprise and not ProductTierType.Teams)
{
throw new NotFoundException();
}
@ -542,7 +550,8 @@ public class OrganizationsController : Controller
}
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")]

View File

@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Models.Api;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.Utilities;
using Constants = Bit.Core.Constants;
@ -11,8 +12,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class OrganizationResponseModel : ResponseModel
{
public OrganizationResponseModel(Organization organization, string obj = "organization")
: base(obj)
public OrganizationResponseModel(
Organization organization,
Plan plan,
string obj = "organization") : base(obj)
{
if (organization == null)
{
@ -28,7 +31,8 @@ public class OrganizationResponseModel : ResponseModel
BusinessCountry = organization.BusinessCountry;
BusinessTaxNumber = organization.BusinessTaxNumber;
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;
Seats = organization.Seats;
MaxAutoscaleSeats = organization.MaxAutoscaleSeats;
@ -110,7 +114,9 @@ public class OrganizationResponseModel : ResponseModel
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
{
public OrganizationSubscriptionResponseModel(Organization organization) : base(organization, "organizationSubscription")
public OrganizationSubscriptionResponseModel(
Organization organization,
Plan plan) : base(organization, plan, "organizationSubscription")
{
Expiration = organization.ExpirationDate;
StorageName = organization.Storage.HasValue ?
@ -119,8 +125,11 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB
}
public OrganizationSubscriptionResponseModel(Organization organization, SubscriptionInfo subscription, bool hideSensitiveData)
: this(organization)
public OrganizationSubscriptionResponseModel(
Organization organization,
SubscriptionInfo subscription,
Plan plan,
bool hideSensitiveData) : this(organization, plan)
{
Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
@ -142,7 +151,7 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
}
public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) :
this(organization)
this(organization, (Plan)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.Models.Data;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
@ -37,7 +38,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
UsePasswordManager = organization.UsePasswordManager;
UsersGetPremium = organization.UsersGetPremium;
UseCustomPermissions = organization.UseCustomPermissions;
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise;
UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
SelfHost = organization.SelfHost;
Seats = organization.Seats;
MaxCollections = organization.MaxCollections;
@ -60,7 +61,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
.UsersCanSponsor(organization);
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
ProductTierType = organization.PlanType.GetProductTier();
FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate;
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;

View File

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

View File

@ -4,6 +4,7 @@ using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Repositories;
@ -21,6 +22,7 @@ public class OrganizationBillingController(
IOrganizationBillingService organizationBillingService,
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
IPricingClient pricingClient,
ISubscriberService subscriberService,
IPaymentHistoryService paymentHistoryService,
IUserService userService) : BaseBillingController
@ -279,7 +281,7 @@ public class OrganizationBillingController(
}
var organizationSignup = model.ToOrganizationSignup(user);
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.Plan = plan.Name;
sale.SubscriptionSetup.SkipTrial = true;

View File

@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
@ -45,7 +46,8 @@ public class OrganizationsController(
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
IReferenceEventService referenceEventService,
ISubscriberService subscriberService,
IOrganizationInstallationRepository organizationInstallationRepository)
IOrganizationInstallationRepository organizationInstallationRepository,
IPricingClient pricingClient)
: Controller
{
[HttpGet("{id:guid}/subscription")]
@ -62,26 +64,28 @@ public class OrganizationsController(
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)
{
var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);
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")]
@ -165,7 +169,8 @@ public class OrganizationsController(
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);

View File

@ -2,6 +2,7 @@
using Bit.Api.Billing.Models.Responses;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
@ -20,6 +21,7 @@ namespace Bit.Api.Billing.Controllers;
public class ProviderBillingController(
ICurrentContext currentContext,
ILogger<BaseProviderController> logger,
IPricingClient pricingClient,
IProviderBillingService providerBillingService,
IProviderPlanRepository providerPlanRepository,
IProviderRepository providerRepository,
@ -84,13 +86,25 @@ public class ProviderBillingController(
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 subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription);
var response = ProviderSubscriptionResponse.From(
subscription,
providerPlans,
configuredProviderPlans,
taxInformation,
subscriptionSuspension,
provider);

View File

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

View File

@ -1,6 +1,7 @@
using System.Net;
using Bit.Api.Billing.Public.Models;
using Bit.Api.Models.Public.Response;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
@ -21,19 +22,22 @@ public class OrganizationController : Controller
private readonly IOrganizationRepository _organizationRepository;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly ILogger<OrganizationController> _logger;
private readonly IPricingClient _pricingClient;
public OrganizationController(
IOrganizationService organizationService,
ICurrentContext currentContext,
IOrganizationRepository organizationRepository,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
ILogger<OrganizationController> logger)
ILogger<OrganizationController> logger,
IPricingClient pricingClient)
{
_organizationService = organizationService;
_currentContext = currentContext;
_organizationRepository = organizationRepository;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_logger = logger;
_pricingClient = pricingClient;
}
/// <summary>
@ -140,7 +144,8 @@ public class OrganizationController : Controller
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);
return string.Empty;

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
namespace Bit.Api.Billing.Public.Models;
@ -93,17 +94,17 @@ public class SecretsManagerSubscriptionUpdateModel
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);
UpdateServiceAccounts(organization, 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,
MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts

View File

@ -1,5 +1,5 @@
using Bit.Api.Models.Response;
using Bit.Core.Utilities;
using Bit.Core.Billing.Pricing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -7,13 +7,15 @@ namespace Bit.Api.Controllers;
[Route("plans")]
[Authorize("Web")]
public class PlansController : Controller
public class PlansController(
IPricingClient pricingClient) : Controller
{
[HttpGet("")]
[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);
}
}

View File

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

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
namespace Bit.Api.Models.Request.Organizations;
@ -12,9 +13,9 @@ public class SecretsManagerSubscriptionUpdateRequestModel
public int ServiceAccountAdjustment { 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,
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.StaticStore;
@ -44,6 +46,13 @@ public class PlanResponseModel : ResponseModel
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 ProductTierType ProductTier { get; set; }
public string Name { get; set; }

View File

@ -1,6 +1,7 @@
using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -37,6 +38,7 @@ public class ServiceAccountsController : Controller
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand;
private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand;
private readonly IPricingClient _pricingClient;
public ServiceAccountsController(
ICurrentContext currentContext,
@ -52,7 +54,8 @@ public class ServiceAccountsController : Controller
ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand,
IDeleteServiceAccountsCommand deleteServiceAccountsCommand,
IRevokeAccessTokensCommand revokeAccessTokensCommand)
IRevokeAccessTokensCommand revokeAccessTokensCommand,
IPricingClient pricingClient)
{
_currentContext = currentContext;
_userService = userService;
@ -66,6 +69,7 @@ public class ServiceAccountsController : Controller
_updateServiceAccountCommand = updateServiceAccountCommand;
_deleteServiceAccountsCommand = deleteServiceAccountsCommand;
_revokeAccessTokensCommand = revokeAccessTokensCommand;
_pricingClient = pricingClient;
_createAccessTokenCommand = createAccessTokenCommand;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
}
@ -124,7 +128,9 @@ public class ServiceAccountsController : Controller
if (newServiceAccountSlotsRequired > 0)
{
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);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
}

View File

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

View File

@ -1,15 +1,17 @@
using Bit.Billing.Constants;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Core.Repositories;
using Stripe;
namespace Bit.Billing.Services.Implementations;
public class ProviderEventService(
ILogger<ProviderEventService> logger,
IOrganizationRepository organizationRepository,
IPricingClient pricingClient,
IProviderInvoiceItemRepository providerInvoiceItemRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IProviderPlanRepository providerPlanRepository,
@ -54,7 +56,14 @@ public class ProviderEventService(
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;
@ -76,7 +85,7 @@ public class ProviderEventService(
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
.Where(item => item.PlanName == plan.Name)

View File

@ -2,11 +2,11 @@
using Bit.Billing.Jobs;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Billing.Pricing;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Quartz;
using Stripe;
using Event = Stripe.Event;
@ -27,6 +27,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
private readonly IFeatureService _featureService;
private readonly IOrganizationEnableCommand _organizationEnableCommand;
private readonly IOrganizationDisableCommand _organizationDisableCommand;
private readonly IPricingClient _pricingClient;
public SubscriptionUpdatedHandler(
IStripeEventService stripeEventService,
@ -40,7 +41,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
ISchedulerFactory schedulerFactory,
IFeatureService featureService,
IOrganizationEnableCommand organizationEnableCommand,
IOrganizationDisableCommand organizationDisableCommand)
IOrganizationDisableCommand organizationDisableCommand,
IPricingClient pricingClient)
{
_stripeEventService = stripeEventService;
_stripeEventUtilityService = stripeEventUtilityService;
@ -54,6 +56,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
_featureService = featureService;
_organizationEnableCommand = organizationEnableCommand;
_organizationDisableCommand = organizationDisableCommand;
_pricingClient = pricingClient;
}
/// <summary>
@ -156,7 +159,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
/// </summary>
/// <param name="parsedEvent"></param>
/// <param name="subscription"></param>
private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(Event parsedEvent,
private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(
Event parsedEvent,
Subscription subscription)
{
if (parsedEvent.Data.PreviousAttributes?.items is null)
@ -164,6 +168,22 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
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
.PreviousAttributes
.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.
// If there are changes to any subscription item, Stripe sends every item in the subscription, both
// changed and unchanged.
var previousSubscriptionHasSecretsManager = previousSubscription?.Items is not null &&
previousSubscription.Items.Any(previousItem =>
StaticStore.Plans.Any(p =>
p.SecretsManager is not null &&
p.SecretsManager.StripeSeatPlanId ==
previousItem.Plan.Id));
var previousSubscriptionHasSecretsManager =
previousSubscription?.Items is not null &&
previousSubscription.Items.Any(
previousSubscriptionItem => previousSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
var currentSubscriptionHasSecretsManager = subscription.Items.Any(i =>
StaticStore.Plans.Any(p =>
p.SecretsManager is not null &&
p.SecretsManager.StripeSeatPlanId == i.Plan.Id));
var currentSubscriptionHasSecretsManager =
subscription.Items.Any(
currentSubscriptionItem => currentSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
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.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Stripe;
using Event = Stripe.Event;
@ -16,6 +15,7 @@ public class UpcomingInvoiceHandler(
ILogger<StripeEventProcessor> logger,
IMailService mailService,
IOrganizationRepository organizationRepository,
IPricingClient pricingClient,
IProviderRepository providerRepository,
IStripeFacade stripeFacade,
IStripeEventService stripeEventService,
@ -52,7 +52,9 @@ public class UpcomingInvoiceHandler(
await TryEnableAutomaticTaxAsync(subscription);
if (!HasAnnualPlan(organization))
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
if (!plan.IsAnnual)
{
return;
}
@ -136,7 +138,7 @@ public class UpcomingInvoiceHandler(
{
if (subscription.AutomaticTax.Enabled ||
!subscription.Customer.HasBillingLocation() ||
IsNonTaxableNonUSBusinessUseSubscription(subscription))
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
{
return;
}
@ -150,14 +152,12 @@ public class UpcomingInvoiceHandler(
return;
bool IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription)
async Task<bool> IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription)
{
var familyPriceIds = new List<string>
{
// TODO: Replace with the PricingClient
StaticStore.GetPlan(PlanType.FamiliesAnnually2019).PasswordManager.StripePlanId,
StaticStore.GetPlan(PlanType.FamiliesAnnually).PasswordManager.StripePlanId
};
var familyPriceIds = (await Task.WhenAll(
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
.Select(plan => plan.PasswordManager.StripePlanId);
return localSubscription.Customer.Address.Country != "US" &&
localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
@ -165,6 +165,4 @@ public class UpcomingInvoiceHandler(
!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.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -24,6 +25,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
private readonly ICollectionRepository _collectionRepository;
private readonly IGroupRepository _groupRepository;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
public UpdateOrganizationUserCommand(
IEventService eventService,
@ -34,7 +36,8 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
ICollectionRepository collectionRepository,
IGroupRepository groupRepository,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient)
{
_eventService = eventService;
_organizationService = organizationService;
@ -45,6 +48,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
_collectionRepository = collectionRepository;
_groupRepository = groupRepository;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
}
/// <summary>
@ -128,8 +132,10 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organizationUser.OrganizationId, 1);
if (additionalSmSeatsRequired > 0)
{
var update = new SecretsManagerSubscriptionUpdate(organization, true)
.AdjustSeats(additionalSmSeatsRequired);
// TODO: https://bitwarden.atlassian.net/browse/PM-17012
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true)
.AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
}
}

View File

@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -45,11 +46,12 @@ public class CloudOrganizationSignUpCommand(
IPushRegistrationService pushRegistrationService,
IPushNotificationService pushNotificationService,
ICollectionRepository collectionRepository,
IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand
IDeviceRepository deviceRepository,
IPricingClient pricingClient) : ICloudOrganizationSignUpCommand
{
public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup)
{
var plan = StaticStore.GetPlan(signup.Plan);
var plan = await pricingClient.GetPlanOrThrow(signup.Plan);
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.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -74,6 +75,7 @@ public class OrganizationService : IOrganizationService
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IOrganizationBillingService _organizationBillingService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
public OrganizationService(
IOrganizationRepository organizationRepository,
@ -108,7 +110,8 @@ public class OrganizationService : IOrganizationService
IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IOrganizationBillingService organizationBillingService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -143,6 +146,7 @@ public class OrganizationService : IOrganizationService
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_organizationBillingService = organizationBillingService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
}
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
@ -210,11 +214,7 @@ public class OrganizationService : IOrganizationService
throw new NotFoundException();
}
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
if (plan == null)
{
throw new BadRequestException("Existing plan not found.");
}
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (!plan.PasswordManager.HasAdditionalStorageOption)
{
@ -268,7 +268,7 @@ public class OrganizationService : IOrganizationService
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)
{
throw new BadRequestException("Existing plan not found.");
@ -320,11 +320,7 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("No subscription found.");
}
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
if (plan == null)
{
throw new BadRequestException("Existing plan not found.");
}
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
if (!plan.PasswordManager.HasAdditionalSeatsOption)
{
@ -442,7 +438,7 @@ public class OrganizationService : IOrganizationService
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");
@ -530,17 +526,6 @@ public class OrganizationService : IOrganizationService
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();
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);
if (additionalSmSeatsRequired > 0)
{
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, true)
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, plan, true)
.AdjustSeats(additionalSmSeatsRequired);
}
@ -1008,7 +994,8 @@ public class OrganizationService : IOrganizationService
if (initialSmSeatCount.HasValue && currentOrganization.SmSeats.HasValue &&
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
};
@ -2237,13 +2224,6 @@ public class OrganizationService : IOrganizationService
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.Enabled = false;
organization.Status = OrganizationStatusType.Pending;

View File

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

View File

@ -10,6 +10,17 @@ namespace Bit.Core.Billing.Extensions;
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) =>
provider is
{

View File

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

View File

@ -3,11 +3,11 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Migration.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
using Stripe;
using Plan = Bit.Core.Models.StaticStore.Plan;
@ -19,6 +19,7 @@ public class OrganizationMigrator(
ILogger<OrganizationMigrator> logger,
IMigrationTrackerCache migrationTrackerCache,
IOrganizationRepository organizationRepository,
IPricingClient pricingClient,
IStripeAdapter stripeAdapter) : IOrganizationMigrator
{
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",
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);
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
@ -206,7 +207,7 @@ public class OrganizationMigrator(
? StripeConstants.CollectionMethod.ChargeAutomatically
: StripeConstants.CollectionMethod.SendInvoice;
var plan = StaticStore.GetPlan(organization.PlanType);
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
var items = new List<SubscriptionItemOptions>
{
@ -279,7 +280,7 @@ public class OrganizationMigrator(
throw new Exception();
}
var plan = StaticStore.GetPlan(migrationRecord.PlanType);
var plan = await pricingClient.GetPlanOrThrow(migrationRecord.PlanType);
ResetOrganizationPlan(organization, plan);
organization.MaxStorageGb = migrationRecord.MaxStorageGb;

View File

@ -1,24 +1,11 @@
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Models.StaticStore;
namespace Bit.Core.Billing.Models;
public record ConfiguredProviderPlan(
Guid Id,
Guid ProviderId,
PlanType PlanType,
Plan Plan,
int SeatMinimum,
int PurchasedSeats,
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;
}
int AssignedSeats);

View File

@ -10,4 +10,17 @@ public record OrganizationMetadata(
bool IsSubscriptionCanceled,
DateTime? InvoiceDueDate,
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)
{
var plan = Core.Utilities.StaticStore.GetPlan(upgrade.Plan);
var passwordManagerOptions = new SubscriptionSetup.PasswordManager
{
Seats = upgrade.AdditionalSeats,
@ -95,7 +93,7 @@ public class OrganizationSale
return new SubscriptionSetup
{
Plan = plan,
PlanType = upgrade.Plan,
PasswordManagerOptions = passwordManagerOptions,
SecretsManagerOptions = secretsManagerOptions
};

View File

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

View File

@ -1,5 +1,7 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.StaticStore;
using Bit.Core.Utilities;
#nullable enable
@ -7,6 +9,30 @@ namespace Bit.Core.Billing.Pricing;
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);
/// <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();
}

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

View File

@ -1,12 +1,13 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Models.StaticStore;
using System.Net;
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.Settings;
using Bit.Core.Utilities;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Grpc.Net.Client;
using Proto.Billing.Pricing;
using Microsoft.Extensions.Logging;
using Plan = Bit.Core.Models.StaticStore.Plan;
#nullable enable
@ -14,10 +15,17 @@ namespace Bit.Core.Billing.Pricing;
public class PricingClient(
IFeatureService featureService,
GlobalSettings globalSettings) : IPricingClient
GlobalSettings globalSettings,
HttpClient httpClient,
ILogger<PricingClient> logger) : IPricingClient
{
public async Task<Plan?> GetPlan(PlanType planType)
{
if (globalSettings.SelfHosted)
{
return null;
}
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
if (!usePricingService)
@ -25,30 +33,55 @@ public class PricingClient(
return StaticStore.GetPlan(planType);
}
using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri);
var client = new PasswordManager.PasswordManagerClient(channel);
var lookupKey = GetLookupKey(planType);
var lookupKey = ToLookupKey(planType);
if (string.IsNullOrEmpty(lookupKey))
if (lookupKey == null)
{
logger.LogError("Could not find Pricing Service lookup key for PlanType {PlanType}", planType);
return null;
}
try
{
var response =
await client.GetPlanByLookupKeyAsync(new GetPlanByLookupKeyRequest { LookupKey = lookupKey });
var response = await httpClient.GetAsync($"plans/lookup/{lookupKey}");
return new PlanAdapter(response);
}
catch (RpcException rpcException) when (rpcException.StatusCode == StatusCode.NotFound)
if (response.IsSuccessStatusCode)
{
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;
}
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()
{
if (globalSettings.SelfHosted)
{
return [];
}
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
if (!usePricingService)
@ -56,14 +89,23 @@ public class PricingClient(
return StaticStore.Plans.ToList();
}
using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri);
var client = new PasswordManager.PasswordManagerClient(channel);
var response = await httpClient.GetAsync("plans");
var response = await client.ListPlansAsync(new Empty());
return response.Plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList();
if (response.IsSuccessStatusCode)
{
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.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.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Braintree;
using Microsoft.Extensions.Logging;
using Stripe;
@ -26,6 +26,7 @@ public class OrganizationBillingService(
IGlobalSettings globalSettings,
ILogger<OrganizationBillingService> logger,
IOrganizationRepository organizationRepository,
IPricingClient pricingClient,
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService,
@ -63,13 +64,22 @@ public class OrganizationBillingService(
return null;
}
var isEligibleForSelfHost = IsEligibleForSelfHost(organization);
if (globalSettings.SelfHosted)
{
return OrganizationMetadata.Default;
}
var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization);
var isManaged = organization.Status == OrganizationStatusType.Managed;
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false,
false, false, false, false, null, null, null);
return OrganizationMetadata.Default with
{
IsEligibleForSelfHost = isEligibleForSelfHost,
IsManaged = isManaged
};
}
var customer = await subscriberService.GetCustomer(organization,
@ -77,18 +87,21 @@ public class OrganizationBillingService(
var subscription = await subscriberService.GetSubscription(organization);
var isOnSecretsManagerStandalone = 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;
var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription);
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone,
isSubscriptionUnpaid, hasSubscription, hasOpenInvoice, isSubscriptionCanceled, invoiceDueDate, invoiceCreatedDate, subPeriodEndDate);
var invoice = await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions());
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(
@ -299,7 +312,7 @@ public class OrganizationBillingService(
Customer customer,
SubscriptionSetup subscriptionSetup)
{
var plan = subscriptionSetup.Plan;
var plan = await pricingClient.GetPlanOrThrow(subscriptionSetup.PlanType);
var passwordManagerOptions = subscriptionSetup.PasswordManagerOptions;
@ -385,15 +398,17 @@ public class OrganizationBillingService(
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
}
private static bool IsEligibleForSelfHost(
private async Task<bool> IsEligibleForSelfHostAsync(
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);
}
private static bool IsOnSecretsManagerStandalone(
private async Task<bool> IsOnSecretsManagerStandalone(
Organization organization,
Customer? customer,
Subscription? subscription)
@ -403,7 +418,7 @@ public class OrganizationBillingService(
return false;
}
var plan = StaticStore.GetPlan(organization.PlanType);
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
if (!plan.SupportsSecretsManager)
{
@ -424,38 +439,5 @@ public class OrganizationBillingService(
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
}

View File

@ -27,12 +27,6 @@
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.85" />
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
<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="Azure.Messaging.ServiceBus" Version="7.18.1" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
@ -78,11 +72,7 @@
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Billing\Pricing\Protos\password-manager.proto" GrpcServices="Client" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
<Folder Include="Properties\" />

View File

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

View File

@ -2,6 +2,7 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Stripe;
using Plan = Bit.Core.Models.StaticStore.Plan;
namespace Bit.Core.Models.Business;
@ -14,18 +15,16 @@ public class ProviderSubscriptionUpdate : SubscriptionUpdate
protected override List<string> PlanIds => [_planId];
public ProviderSubscriptionUpdate(
PlanType planType,
Plan plan,
int previouslyPurchasedSeats,
int newlyPurchasedSeats)
{
if (!planType.SupportsConsolidatedBilling())
if (!plan.Type.SupportsConsolidatedBilling())
{
throw new BillingException(
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;
_previouslyPurchasedSeats = previouslyPurchasedSeats;
_newlyPurchasedSeats = newlyPurchasedSeats;

View File

@ -7,6 +7,7 @@ namespace Bit.Core.Models.Business;
public class SecretsManagerSubscriptionUpdate
{
public Organization Organization { get; }
public Plan Plan { get; }
/// <summary>
/// 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 MaxAutoscaleSmServiceAccountsChanged =>
MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts;
public Plan Plan => Utilities.StaticStore.GetPlan(Organization.PlanType);
public bool SmSeatAutoscaleLimitReached => SmSeats.HasValue && MaxAutoscaleSmSeats.HasValue && SmSeats == MaxAutoscaleSmSeats;
public bool SmServiceAccountAutoscaleLimitReached => SmServiceAccounts.HasValue &&
MaxAutoscaleSmServiceAccounts.HasValue &&
SmServiceAccounts == MaxAutoscaleSmServiceAccounts;
public SecretsManagerSubscriptionUpdate(Organization organization, bool autoscaling)
public SecretsManagerSubscriptionUpdate(Organization organization, Plan plan, bool autoscaling)
{
if (organization == null)
{
throw new NotFoundException("Organization is not found.");
}
Organization = organization;
Organization = organization ?? throw new NotFoundException("Organization is not found.");
Plan = plan;
if (!Plan.SupportsSecretsManager)
{

View File

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

View File

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

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
@ -50,9 +51,10 @@ public class SetUpSponsorshipCommand : ISetUpSponsorshipCommand
// Check org to sponsor's product type
var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductTierType;
var sponsoredOrganizationProductTier = sponsoredOrganization.PlanType.GetProductTier();
if (requiredSponsoredProductType == null ||
sponsoredOrganization == null ||
StaticStore.GetPlan(sponsoredOrganization.PlanType).ProductTier != requiredSponsoredProductType.Value)
sponsoredOrganizationProductTier != requiredSponsoredProductType.Value)
{
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.Billing.Extensions;
using Bit.Core.Entities;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
@ -103,8 +104,6 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo
return false;
}
var sponsoringOrgPlan = Utilities.StaticStore.GetPlan(sponsoringOrganization.PlanType);
if (OrgDisabledForMoreThanGracePeriod(sponsoringOrganization))
{
_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;
}
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);
await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Extensions;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -31,9 +32,10 @@ public class CreateSponsorshipCommand : ICreateSponsorshipCommand
}
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductTierType;
var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier();
if (requiredSponsoringProductType == null ||
sponsoringOrg == null ||
StaticStore.GetPlan(sponsoringOrg.PlanType).ProductTier != requiredSponsoringProductType.Value)
sponsoringOrgProductTier != requiredSponsoringProductType.Value)
{
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.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
@ -15,22 +15,25 @@ public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscripti
private readonly IPaymentService _paymentService;
private readonly IOrganizationService _organizationService;
private readonly IProviderRepository _providerRepository;
private readonly IPricingClient _pricingClient;
public AddSecretsManagerSubscriptionCommand(
IPaymentService paymentService,
IOrganizationService organizationService,
IProviderRepository providerRepository)
IProviderRepository providerRepository,
IPricingClient pricingClient)
{
_paymentService = paymentService;
_organizationService = organizationService;
_providerRepository = providerRepository;
_pricingClient = pricingClient;
}
public async Task SignUpAsync(Organization organization, int additionalSmSeats,
int additionalServiceAccounts)
{
await ValidateOrganization(organization);
var plan = StaticStore.GetPlan(organization.PlanType);
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var signup = SetOrganizationUpgrade(organization, additionalSmSeats, additionalServiceAccounts);
_organizationService.ValidateSecretsManagerPlan(plan, signup);
@ -73,7 +76,13 @@ public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscripti
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)
{
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.Billing.Enums;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Enums;
@ -18,7 +19,6 @@ using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
@ -38,6 +38,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
private readonly IOrganizationService _organizationService;
private readonly IFeatureService _featureService;
private readonly IOrganizationBillingService _organizationBillingService;
private readonly IPricingClient _pricingClient;
public UpgradeOrganizationPlanCommand(
IOrganizationUserRepository organizationUserRepository,
@ -53,7 +54,8 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
IFeatureService featureService,
IOrganizationBillingService organizationBillingService)
IOrganizationBillingService organizationBillingService,
IPricingClient pricingClient)
{
_organizationUserRepository = organizationUserRepository;
_collectionRepository = collectionRepository;
@ -69,6 +71,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
_organizationService = organizationService;
_featureService = featureService;
_organizationBillingService = organizationBillingService;
_pricingClient = pricingClient;
}
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.");
}
var existingPlan = StaticStore.GetPlan(organization.PlanType);
if (existingPlan == null)
{
throw new BadRequestException("Existing plan not found.");
}
var existingPlan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
if (newPlan == null)
var newPlan = await _pricingClient.GetPlanOrThrow(upgrade.Plan);
if (newPlan.Disabled)
{
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.Responses;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -37,6 +38,7 @@ public class StripePaymentService : IPaymentService
private readonly IFeatureService _featureService;
private readonly ITaxService _taxService;
private readonly ISubscriberService _subscriberService;
private readonly IPricingClient _pricingClient;
public StripePaymentService(
ITransactionRepository transactionRepository,
@ -46,7 +48,8 @@ public class StripePaymentService : IPaymentService
IGlobalSettings globalSettings,
IFeatureService featureService,
ITaxService taxService,
ISubscriberService subscriberService)
ISubscriberService subscriberService,
IPricingClient pricingClient)
{
_transactionRepository = transactionRepository;
_logger = logger;
@ -56,6 +59,7 @@ public class StripePaymentService : IPaymentService
_featureService = featureService;
_taxService = taxService;
_subscriberService = subscriberService;
_pricingClient = pricingClient;
}
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
@ -297,7 +301,7 @@ public class StripePaymentService : IPaymentService
OrganizationSponsorship sponsorship,
bool applySponsorship)
{
var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType);
var existingPlan = await _pricingClient.GetPlanOrThrow(org.PlanType);
var sponsoredPlan = sponsorship?.PlanSponsorshipType != null ?
Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) :
null;
@ -887,18 +891,21 @@ public class StripePaymentService : IPaymentService
return paymentIntentClientSecret;
}
public Task<string> AdjustSubscription(
public async Task<string> AdjustSubscription(
Organization organization,
StaticStore.Plan updatedPlan,
int newlyPurchasedPasswordManagerSeats,
bool subscribedToSecretsManager,
int? newlyPurchasedSecretsManagerSeats,
int? newlyPurchasedAdditionalSecretsManagerServiceAccounts,
int newlyPurchasedAdditionalStorage) =>
FinalizeSubscriptionChangeAsync(
int newlyPurchasedAdditionalStorage)
{
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
return await FinalizeSubscriptionChangeAsync(
organization,
new CompleteSubscriptionUpdate(
organization,
plan,
new SubscriptionData
{
Plan = updatedPlan,
@ -909,6 +916,7 @@ public class StripePaymentService : IPaymentService
newlyPurchasedAdditionalSecretsManagerServiceAccounts,
PurchasedAdditionalStorage = newlyPurchasedAdditionalStorage
}), true);
}
public Task<string> AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats) =>
FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats));
@ -921,7 +929,7 @@ public class StripePaymentService : IPaymentService
=> FinalizeSubscriptionChangeAsync(
provider,
new ProviderSubscriptionUpdate(
plan.Type,
plan,
currentlySubscribedSeats,
newlySubscribedSeats));
@ -1957,7 +1965,7 @@ public class StripePaymentService : IPaymentService
string gatewayCustomerId,
string gatewaySubscriptionId)
{
var plan = Utilities.StaticStore.GetPlan(parameters.PasswordManager.Plan);
var plan = await _pricingClient.GetPlanOrThrow(parameters.PasswordManager.Plan);
var options = new InvoiceCreatePreviewOptions
{

View File

@ -1,5 +1,6 @@
using System.Collections.Immutable;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -137,6 +138,7 @@ public static class StaticStore
}
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<SponsoredPlan> SponsoredPlans { get; set; } = new[]
{
@ -147,10 +149,11 @@ public static class StaticStore
SponsoringProductTierType = ProductTierType.Enterprise,
StripePlanId = "2021-family-for-enterprise-annually",
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 SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>
@ -167,6 +170,7 @@ public static class StaticStore
/// </returns>
public static bool IsAddonSubscriptionItem(string stripePlanId)
{
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-16844
return Plans.Any(p =>
p.PasswordManager.StripeStoragePlanId == 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.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -54,6 +55,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
private readonly IPricingClient _pricingClient;
private readonly OrganizationsController _sut;
public OrganizationsControllerTests()
@ -78,6 +80,7 @@ public class OrganizationsControllerTests : IDisposable
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
_cloudOrganizationSignUpCommand = Substitute.For<ICloudOrganizationSignUpCommand>();
_organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>();
_pricingClient = Substitute.For<IPricingClient>();
_sut = new OrganizationsController(
_organizationRepository,
@ -99,7 +102,8 @@ public class OrganizationsControllerTests : IDisposable
_orgDeleteTokenDataFactory,
_removeOrganizationUserCommand,
_cloudOrganizationSignUpCommand,
_organizationDeleteCommand);
_organizationDeleteCommand,
_pricingClient);
}
public void Dispose()

View File

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

View File

@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
@ -331,6 +332,11 @@ public class ProviderBillingControllerTests
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);
Assert.IsType<Ok<ProviderSubscriptionResponse>>(result);

View File

@ -2,6 +2,7 @@
using Bit.Api.SecretsManager.Controllers;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Enums;
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.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
@ -119,6 +121,8 @@ public class ServiceAccountsControllerTests
{
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.GetDependency<ICreateServiceAccountCommand>().Received(1)

View File

@ -1,14 +1,16 @@
using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Billing.Test.Utilities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Stripe;
using Xunit;
@ -17,6 +19,12 @@ namespace Bit.Billing.Test.Services;
public class ProviderEventServiceTests
{
private readonly IOrganizationRepository _organizationRepository =
Substitute.For<IOrganizationRepository>();
private readonly IPricingClient _pricingClient =
Substitute.For<IPricingClient>();
private readonly IProviderInvoiceItemRepository _providerInvoiceItemRepository =
Substitute.For<IProviderInvoiceItemRepository>();
@ -37,7 +45,8 @@ public class ProviderEventServiceTests
public ProviderEventServiceTests()
{
_providerEventService = new ProviderEventService(
Substitute.For<ILogger<ProviderEventService>>(),
_organizationRepository,
_pricingClient,
_providerInvoiceItemRepository,
_providerOrganizationRepository,
_providerPlanRepository,
@ -147,6 +156,12 @@ public class ProviderEventServiceTests
_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>
{
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);
// Act

View File

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

View File

@ -10,6 +10,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -203,10 +204,12 @@ public class OrganizationServiceTests
{
signup.Plan = PlanType.TeamsMonthly;
var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup);
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 =>
org.Id == organization.Id &&
org.Name == signup.Name &&
@ -894,6 +897,8 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
SetupOrgUserRepositoryCreateManyAsyncMock(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.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().Received(1)
@ -933,6 +938,9 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
sutProvider.GetDependency<IReferenceEventService>().RaiseEventAsync(default)
.ThrowsForAnyArgs<BadRequestException>();
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
await Assert.ThrowsAsync<AggregateException>(async () =>
await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites));
@ -1338,6 +1346,9 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
organization.MaxAutoscaleSeats = currentMaxAutoscaleSeats;
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,
seatAdjustment, maxAutoscaleSeats));
@ -1360,6 +1371,9 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
organization.Seats = 100;
organization.SmSeats = 100;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
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.Billing.Constants;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -15,43 +17,6 @@ namespace Bit.Core.Test.Billing.Services;
public class OrganizationBillingServiceTests
{
#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]
public async Task GetMetadata_Succeeds(
@ -61,6 +26,11 @@ public class OrganizationBillingServiceTests
{
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>();
subscriberService
@ -99,7 +69,8 @@ public class OrganizationBillingServiceTests
var metadata = await sutProvider.Sut.GetMetadata(organizationId);
Assert.True(metadata.IsOnSecretsManagerStandalone);
Assert.True(metadata!.IsOnSecretsManagerStandalone);
}
#endregion
}

View File

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

View File

@ -2,7 +2,9 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
@ -11,19 +13,40 @@ namespace Bit.Core.Test.Models.Business;
[SecretsManagerOrganizationCustomize]
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]
[BitAutoData(PlanType.Custom)]
[BitAutoData(PlanType.FamiliesAnnually)]
[BitAutoData(PlanType.FamiliesAnnually2019)]
[BitMemberAutoData(nameof(NonSmPlans))]
public Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
PlanType planType,
Plan plan,
Organization organization)
{
// Arrange
organization.PlanType = planType;
organization.PlanType = plan.Type;
// Act
var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, false));
var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, plan, false));
// Assert
Assert.Contains("Invalid Secrets Manager plan", exception.Message, StringComparison.InvariantCultureIgnoreCase);
@ -31,28 +54,16 @@ public class SecretsManagerSubscriptionUpdateTests
}
[Theory]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[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)]
[BitMemberAutoData(nameof(SmPlans))]
public void UpdateSubscription_WithNonSecretsManagerPlanType_DoesNotThrowException(
PlanType planType,
Plan plan,
Organization organization)
{
// Arrange
organization.PlanType = planType;
organization.PlanType = plan.Type;
// Act
var ex = Record.Exception(() => new SecretsManagerSubscriptionUpdate(organization, false));
var ex = Record.Exception(() => new SecretsManagerSubscriptionUpdate(organization, plan, false));
// Assert
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.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
@ -41,7 +42,8 @@ public class AddSecretsManagerSubscriptionCommandTests
{
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);
@ -85,6 +87,8 @@ public class AddSecretsManagerSubscriptionCommandTests
{
organization.GatewayCustomerId = null;
organization.PlanType = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts));
Assert.Contains("No payment method found.", exception.Message);
@ -101,6 +105,8 @@ public class AddSecretsManagerSubscriptionCommandTests
{
organization.GatewaySubscriptionId = null;
organization.PlanType = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts));
Assert.Contains("No subscription found.", exception.Message);
@ -132,6 +138,8 @@ public class AddSecretsManagerSubscriptionCommandTests
organization.UseSecretsManager = false;
provider.Type = ProviderType.Msp;
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>(
() => sutProvider.Sut.SignUpAsync(organization, 10, 10));

View File

@ -21,26 +21,48 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationSubscriptionUpdate;
[SecretsManagerOrganizationCustomize]
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]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[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)]
[BitMemberAutoData(nameof(AllTeamsAndEnterprise))]
public async Task UpdateSubscriptionAsync_UpdateEverything_ValidInput_Passes(
PlanType planType,
Plan plan,
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
organization.PlanType = planType;
organization.PlanType = plan.Type;
organization.Seats = 400;
organization.SmSeats = 10;
organization.MaxAutoscaleSmSeats = 20;
@ -52,7 +74,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var updateMaxAutoscaleSmSeats = 16;
var updateMaxAutoscaleSmServiceAccounts = 301;
var update = new SecretsManagerSubscriptionUpdate(organization, false)
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{
SmSeats = updateSmSeats,
SmServiceAccounts = updateSmServiceAccounts,
@ -62,7 +84,6 @@ public class UpdateSecretsManagerSubscriptionCommandTests
await sutProvider.Sut.UpdateSubscriptionAsync(update);
var plan = StaticStore.GetPlan(organization.PlanType);
await sutProvider.GetDependency<IPaymentService>().Received(1)
.AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase);
await sutProvider.GetDependency<IPaymentService>().Received(1)
@ -83,17 +104,13 @@ public class UpdateSecretsManagerSubscriptionCommandTests
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.TeamsStarter)]
[BitMemberAutoData(nameof(CurrentTeamsAndEnterprise))]
public async Task UpdateSubscriptionAsync_ValidInput_WithNullMaxAutoscale_Passes(
PlanType planType,
Plan plan,
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
organization.PlanType = planType;
organization.PlanType = plan.Type;
organization.Seats = 20;
const int updateSmSeats = 15;
@ -102,7 +119,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
// Ensure that SmSeats is different from the original organization.SmSeats
organization.SmSeats = updateSmSeats + 5;
var update = new SecretsManagerSubscriptionUpdate(organization, false)
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{
SmSeats = updateSmSeats,
MaxAutoscaleSmSeats = null,
@ -112,7 +129,6 @@ public class UpdateSecretsManagerSubscriptionCommandTests
await sutProvider.Sut.UpdateSubscriptionAsync(update);
var plan = StaticStore.GetPlan(organization.PlanType);
await sutProvider.GetDependency<IPaymentService>().Received(1)
.AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase);
await sutProvider.GetDependency<IPaymentService>().Received(1)
@ -141,7 +157,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization,
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);
@ -156,8 +173,10 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider,
Organization organization)
{
var plan = StaticStore.GetPlan(organization.PlanType);
organization.UseSecretsManager = false;
var update = new SecretsManagerSubscriptionUpdate(organization, false);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateSubscriptionAsync(update));
@ -167,27 +186,16 @@ public class UpdateSecretsManagerSubscriptionCommandTests
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[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)]
[BitMemberAutoData(nameof(AllTeamsAndEnterprise))]
public async Task UpdateSubscriptionAsync_PaidPlan_NullGatewayCustomerId_ThrowsException(
PlanType planType,
Plan plan,
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
organization.PlanType = planType;
organization.PlanType = plan.Type;
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));
Assert.Contains("No payment method found.", exception.Message);
@ -195,27 +203,15 @@ public class UpdateSecretsManagerSubscriptionCommandTests
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[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)]
[BitMemberAutoData(nameof(AllTeamsAndEnterprise))]
public async Task UpdateSubscriptionAsync_PaidPlan_NullGatewaySubscriptionId_ThrowsException(
PlanType planType,
Plan plan,
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
organization.PlanType = planType;
organization.PlanType = plan.Type;
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));
Assert.Contains("No subscription found.", exception.Message);
@ -223,24 +219,12 @@ public class UpdateSecretsManagerSubscriptionCommandTests
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[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 AdjustServiceAccountsAsync_WithEnterpriseOrTeamsPlans_Success(PlanType planType, Guid organizationId,
[BitMemberAutoData(nameof(AllTeamsAndEnterprise))]
public async Task AdjustServiceAccountsAsync_WithEnterpriseOrTeamsPlans_Success(
Plan plan,
Guid organizationId,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
var plan = StaticStore.GetPlan(planType);
var organizationSeats = plan.SecretsManager.BaseSeats + 10;
var organizationMaxAutoscaleSeats = 20;
var organizationServiceAccounts = plan.SecretsManager.BaseServiceAccount + 10;
@ -249,7 +233,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var organization = new Organization
{
Id = organizationId,
PlanType = planType,
PlanType = plan.Type,
GatewayCustomerId = "1",
GatewaySubscriptionId = "2",
UseSecretsManager = true,
@ -263,7 +247,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var expectedSmServiceAccounts = organizationServiceAccounts + smServiceAccountsAdjustment;
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);
@ -290,8 +274,9 @@ public class UpdateSecretsManagerSubscriptionCommandTests
// Make sure Password Manager seats is greater or equal to Secrets Manager seats
organization.Seats = seatCount;
var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, false)
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{
SmSeats = seatCount,
MaxAutoscaleSmSeats = seatCount
@ -310,7 +295,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
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>(
() => sutProvider.Sut.UpdateSubscriptionAsync(update));
@ -325,7 +311,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization,
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));
Assert.Contains("Cannot use autoscaling to subtract seats.", exception.Message);
@ -340,7 +327,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
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));
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.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));
Assert.Contains("Secrets Manager seat limit has been reached.", exception.Message);
@ -370,7 +359,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization,
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,
MaxAutoscaleSmSeats = organization.SmSeats + 5
@ -388,7 +378,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
var update = new SecretsManagerSubscriptionUpdate(organization, false)
var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{
SmSeats = 0,
};
@ -407,7 +398,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
organization.SmSeats = 8;
var update = new SecretsManagerSubscriptionUpdate(organization, false)
var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{
SmSeats = 7,
};
@ -425,7 +417,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
var update = new SecretsManagerSubscriptionUpdate(organization, false)
var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{
SmServiceAccounts = 300,
MaxAutoscaleSmServiceAccounts = 300
@ -444,7 +437,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
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));
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,
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));
Assert.Contains("Cannot use autoscaling to subtract machine accounts.", exception.Message);
@ -472,7 +467,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
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));
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.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));
Assert.Contains("Secrets Manager machine account limit has been reached.", exception.Message);
@ -508,7 +505,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
organization.SmServiceAccounts = smServiceAccount - 5;
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,
MaxAutoscaleSmServiceAccounts = maxAutoscaleSmServiceAccounts
@ -530,7 +528,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
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,
};
@ -542,28 +541,16 @@ public class UpdateSecretsManagerSubscriptionCommandTests
}
[Theory]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[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)]
[BitMemberAutoData(nameof(AllTeamsAndEnterprise))]
public async Task UpdateSmServiceAccounts_WhenCurrentServiceAccountsIsGreaterThanNew_ThrowsBadRequestException(
PlanType planType,
Plan plan,
Organization organization,
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
{
var currentServiceAccounts = 301;
organization.PlanType = planType;
organization.PlanType = plan.Type;
organization.SmServiceAccounts = currentServiceAccounts;
var update = new SecretsManagerSubscriptionUpdate(organization, false) { SmServiceAccounts = 201 };
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = 201 };
sutProvider.GetDependency<IServiceAccountRepository>()
.GetServiceAccountCountByOrganizationIdAsync(organization.Id)
@ -586,7 +573,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
organization.SmSeats = smSeats - 1;
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,
MaxAutoscaleSmSeats = maxAutoscaleSmSeats
@ -606,7 +594,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
{
organization.PlanType = planType;
organization.SmSeats = 2;
var update = new SecretsManagerSubscriptionUpdate(organization, false)
var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{
MaxAutoscaleSmSeats = 3
};
@ -625,7 +614,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
{
organization.PlanType = planType;
organization.SmSeats = 2;
var update = new SecretsManagerSubscriptionUpdate(organization, false)
var plan = StaticStore.GetPlan(organization.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
{
MaxAutoscaleSmSeats = 2
};
@ -645,7 +635,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests
organization.PlanType = planType;
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));
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.Pricing;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
@ -43,6 +44,7 @@ public class UpgradeOrganizationPlanCommandTests
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
upgrade.Plan = organization.PlanType;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
@ -58,6 +60,7 @@ public class UpgradeOrganizationPlanCommandTests
upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalServiceAccounts = 10;
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.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("already on this plan", exception.Message);
@ -69,9 +72,11 @@ public class UpgradeOrganizationPlanCommandTests
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
upgrade.AdditionalSmSeats = 10;
upgrade.AdditionalSeats = 10;
upgrade.Plan = PlanType.TeamsAnnually;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);
}
@ -92,6 +97,8 @@ public class UpgradeOrganizationPlanCommandTests
organization.PlanType = PlanType.FamiliesAnnually;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
organizationUpgrade.AdditionalSeats = 30;
organizationUpgrade.UseSecretsManager = true;
organizationUpgrade.AdditionalSmSeats = 20;
@ -99,6 +106,8 @@ public class UpgradeOrganizationPlanCommandTests
organizationUpgrade.AdditionalStorageGb = 3;
organizationUpgrade.Plan = planType;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organizationUpgrade.Plan).Returns(StaticStore.GetPlan(organizationUpgrade.Plan));
await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade);
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSubscription(
organization,
@ -120,7 +129,10 @@ public class UpgradeOrganizationPlanCommandTests
public async Task UpgradePlan_SM_Passes(PlanType planType, Organization organization, OrganizationUpgrade upgrade,
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
{
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
upgrade.Plan = planType;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
var plan = StaticStore.GetPlan(upgrade.Plan);
@ -155,8 +167,10 @@ public class UpgradeOrganizationPlanCommandTests
upgrade.AdditionalSeats = 15;
upgrade.AdditionalSmSeats = 1;
upgrade.AdditionalServiceAccounts = 0;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
organization.SmSeats = 2;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IOrganizationUserRepository>()
@ -181,9 +195,11 @@ public class UpgradeOrganizationPlanCommandTests
upgrade.AdditionalSeats = 15;
upgrade.AdditionalSmSeats = 1;
upgrade.AdditionalServiceAccounts = 0;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan));
organization.SmSeats = 1;
organization.SmServiceAccounts = currentServiceAccounts;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType));
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IOrganizationUserRepository>()