1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-02 18:22:19 -05:00
bitwarden/src/Core/Billing/Pricing/PlanAdapter.cs
Alex Morask a2e665cb96
[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
2025-02-27 07:55:46 -05:00

216 lines
9.7 KiB
C#

using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing.Models;
using Bit.Core.Models.StaticStore;
#nullable enable
namespace Bit.Core.Billing.Pricing;
public record PlanAdapter : Plan
{
public PlanAdapter(PlanDTO plan)
{
Type = ToPlanType(plan.LookupKey);
ProductTier = ToProductTierType(Type);
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");
HasDirectory = HasFeature("directory");
HasEvents = HasFeature("events");
HasTotp = HasFeature("totp");
Has2fa = HasFeature("2fa");
HasApi = HasFeature("api");
HasSso = HasFeature("sso");
HasKeyConnector = HasFeature("keyConnector");
HasScim = HasFeature("scim");
HasResetPassword = HasFeature("resetPassword");
UsersGetPremium = HasFeature("usersGetPremium");
UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder)
? int.Parse(upgradeSortOrder)
: 0;
DisplaySortOrder = plan.AdditionalData.TryGetValue("displaySortOrder", out var displaySortOrder)
? int.Parse(displaySortOrder)
: 0;
Disabled = !plan.Available;
LegacyYear = plan.LegacyYear;
PasswordManager = ToPasswordManagerPlanFeatures(plan);
SecretsManager = plan.SecretsManager != null ? ToSecretsManagerPlanFeatures(plan) : null;
return;
bool HasFeature(string lookupKey) => plan.Features.Any(feature => feature.LookupKey == lookupKey);
}
#region Mappings
private static PlanType ToPlanType(string lookupKey)
=> lookupKey switch
{
"enterprise-annually" => PlanType.EnterpriseAnnually,
"enterprise-annually-2019" => PlanType.EnterpriseAnnually2019,
"enterprise-annually-2020" => PlanType.EnterpriseAnnually2020,
"enterprise-annually-2023" => PlanType.EnterpriseAnnually2023,
"enterprise-monthly" => PlanType.EnterpriseMonthly,
"enterprise-monthly-2019" => PlanType.EnterpriseMonthly2019,
"enterprise-monthly-2020" => PlanType.EnterpriseMonthly2020,
"enterprise-monthly-2023" => PlanType.EnterpriseMonthly2023,
"families" => PlanType.FamiliesAnnually,
"families-2019" => PlanType.FamiliesAnnually2019,
"free" => PlanType.Free,
"teams-annually" => PlanType.TeamsAnnually,
"teams-annually-2019" => PlanType.TeamsAnnually2019,
"teams-annually-2020" => PlanType.TeamsAnnually2020,
"teams-annually-2023" => PlanType.TeamsAnnually2023,
"teams-monthly" => PlanType.TeamsMonthly,
"teams-monthly-2019" => PlanType.TeamsMonthly2019,
"teams-monthly-2020" => PlanType.TeamsMonthly2020,
"teams-monthly-2023" => PlanType.TeamsMonthly2023,
"teams-starter" => PlanType.TeamsStarter,
"teams-starter-2023" => PlanType.TeamsStarter2023,
_ => throw new BillingException() // TODO: Flesh out
};
private static ProductTierType ToProductTierType(PlanType planType)
=> planType switch
{
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() // TODO: Flesh out
};
private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(PlanDTO plan)
{
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
{
StripePlanId = stripePlanId,
StripeSeatPlanId = stripeSeatPlanId,
StripeProviderPortalSeatPlanId = stripeProviderPortalSeatPlanId,
BasePrice = basePrice,
SeatPrice = seatPrice,
ProviderPortalSeatPrice = providerPortalSeatPrice,
AllowSeatAutoscale = scales,
HasAdditionalSeatsOption = scales,
BaseSeats = baseSeats,
MaxSeats = maxSeats,
BaseStorageGb = baseStorageGb,
HasAdditionalStorageOption = hasAdditionalStorageOption,
AdditionalStoragePricePerGb = additionalStoragePricePerGb,
StripeStoragePlanId = stripeStoragePlanId,
MaxCollections = maxCollections
};
}
private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(PlanDTO plan)
{
var seats = plan.SecretsManager!.Seats;
var serviceAccounts = plan.SecretsManager.ServiceAccounts;
var maxServiceAccounts = GetMaxServiceAccounts(serviceAccounts);
var allowServiceAccountsAutoscale = serviceAccounts.IsScalable;
var stripeServiceAccountPlanId = GetStripeServiceAccountPlanId(serviceAccounts);
var additionalPricePerServiceAccount = GetAdditionalPricePerServiceAccount(serviceAccounts);
var baseServiceAccount = GetBaseServiceAccount(serviceAccounts);
var hasAdditionalServiceAccountOption = serviceAccounts.IsScalable;
var stripeSeatPlanId = GetStripeSeatPlanId(seats);
var hasAdditionalSeatsOption = seats.IsScalable;
var seatPrice = GetSeatPrice(seats);
var maxSeats = GetMaxSeats(seats);
var allowSeatAutoscale = seats.IsScalable;
var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0;
return new SecretsManagerPlanFeatures
{
MaxServiceAccounts = maxServiceAccounts,
AllowServiceAccountsAutoscale = allowServiceAccountsAutoscale,
StripeServiceAccountPlanId = stripeServiceAccountPlanId,
AdditionalPricePerServiceAccount = additionalPricePerServiceAccount,
BaseServiceAccount = baseServiceAccount,
HasAdditionalServiceAccountOption = hasAdditionalServiceAccountOption,
StripeSeatPlanId = stripeSeatPlanId,
HasAdditionalSeatsOption = hasAdditionalSeatsOption,
SeatPrice = seatPrice,
MaxSeats = maxSeats,
AllowSeatAutoscale = allowSeatAutoscale,
MaxProjects = maxProjects
};
}
private static decimal? GetAdditionalPricePerServiceAccount(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.FromScalable(x => x.Price);
private static decimal GetBasePrice(PurchasableDTO purchasable)
=> purchasable.FromPackaged(x => x.Price);
private static int GetBaseSeats(PurchasableDTO purchasable)
=> purchasable.FromPackaged(x => x.Quantity);
private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.Match(
free => (short)free.Quantity,
scalable => (short)scalable.Provided);
private static short? GetMaxSeats(PurchasableDTO purchasable)
=> purchasable.Match<short?>(
free => (short)free.Quantity,
packaged => (short)packaged.Quantity,
_ => null);
private static short? GetMaxSeats(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.FromFree(x => (short)x.Quantity);
private static short? GetMaxServiceAccounts(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.FromFree(x => (short)x.Quantity);
private static decimal GetSeatPrice(PurchasableDTO purchasable)
=> purchasable.Match(
_ => 0,
packaged => packaged.Additional?.Price ?? 0,
scalable => scalable.Price);
private static decimal GetSeatPrice(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.FromScalable(x => x.Price);
private static string? GetStripePlanId(PurchasableDTO purchasable)
=> purchasable.FromPackaged(x => x.StripePriceId);
private static string? GetStripeSeatPlanId(PurchasableDTO purchasable)
=> purchasable.Match(
_ => null,
packaged => packaged.Additional?.StripePriceId,
scalable => scalable.StripePriceId);
private static string? GetStripeSeatPlanId(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.FromScalable(x => x.StripePriceId);
private static string? GetStripeServiceAccountPlanId(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.FromScalable(x => x.StripePriceId);
#endregion
}