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:
parent
bd66f06bd9
commit
a2e665cb96
@ -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);
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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,
|
||||
[],
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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 });
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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")]
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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}")]
|
||||
|
@ -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
|
||||
|
@ -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; }
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
40
src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs
Normal file
40
src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs
Normal 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)
|
||||
);
|
||||
}
|
28
src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs
Normal file
28
src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
9
src/Core/Billing/Pricing/Models/FeatureDTO.cs
Normal file
9
src/Core/Billing/Pricing/Models/FeatureDTO.cs
Normal 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!;
|
||||
}
|
27
src/Core/Billing/Pricing/Models/PlanDTO.cs
Normal file
27
src/Core/Billing/Pricing/Models/PlanDTO.cs
Normal 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!;
|
||||
}
|
73
src/Core/Billing/Pricing/Models/PurchasableDTO.cs
Normal file
73
src/Core/Billing/Pricing/Models/PurchasableDTO.cs
Normal 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";
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
21
src/Core/Billing/Pricing/ServiceCollectionExtensions.cs
Normal file
21
src/Core/Billing/Pricing/ServiceCollectionExtensions.cs
Normal 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");
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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\" />
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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; }
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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.");
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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));
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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>()
|
||||
|
Loading…
Reference in New Issue
Block a user