mirror of
https://github.com/bitwarden/server.git
synced 2025-01-09 19:57:37 +01:00
Merge branch 'main' into pm-15807-Move-subscription-to-canceled-7-days-after-unpai'
This commit is contained in:
commit
8392ddc4c2
@ -34,6 +34,9 @@ public static class ServiceCollectionExtensions
|
|||||||
Url = new Uri("https://github.com/bitwarden/server/blob/master/LICENSE.txt")
|
Url = new Uri("https://github.com/bitwarden/server/blob/master/LICENSE.txt")
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
config.CustomSchemaIds(type => type.FullName);
|
||||||
|
|
||||||
config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" });
|
config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" });
|
||||||
|
|
||||||
config.AddSecurityDefinition("oauth2-client-credentials", new OpenApiSecurityScheme
|
config.AddSecurityDefinition("oauth2-client-credentials", new OpenApiSecurityScheme
|
||||||
|
@ -17,6 +17,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
|
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
|
||||||
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
|
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
|
||||||
services.AddTransient<ISubscriberService, SubscriberService>();
|
services.AddTransient<ISubscriberService, SubscriberService>();
|
||||||
|
// services.AddSingleton<IPricingClient, PricingClient>();
|
||||||
services.AddLicenseServices();
|
services.AddLicenseServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,11 @@ public abstract record Plan
|
|||||||
public ProductTierType ProductTier { get; protected init; }
|
public ProductTierType ProductTier { get; protected init; }
|
||||||
public string Name { get; protected init; }
|
public string Name { get; protected init; }
|
||||||
public bool IsAnnual { get; protected init; }
|
public bool IsAnnual { get; protected init; }
|
||||||
|
// TODO: Move to the client
|
||||||
public string NameLocalizationKey { get; protected init; }
|
public string NameLocalizationKey { get; protected init; }
|
||||||
|
// TODO: Move to the client
|
||||||
public string DescriptionLocalizationKey { get; protected init; }
|
public string DescriptionLocalizationKey { get; protected init; }
|
||||||
|
// TODO: Remove
|
||||||
public bool CanBeUsedByBusiness { get; protected init; }
|
public bool CanBeUsedByBusiness { get; protected init; }
|
||||||
public int? TrialPeriodDays { get; protected init; }
|
public int? TrialPeriodDays { get; protected init; }
|
||||||
public bool HasSelfHost { get; protected init; }
|
public bool HasSelfHost { get; protected init; }
|
||||||
@ -27,7 +30,9 @@ public abstract record Plan
|
|||||||
public bool UsersGetPremium { get; protected init; }
|
public bool UsersGetPremium { get; protected init; }
|
||||||
public bool HasCustomPermissions { get; protected init; }
|
public bool HasCustomPermissions { get; protected init; }
|
||||||
public int UpgradeSortOrder { get; protected init; }
|
public int UpgradeSortOrder { get; protected init; }
|
||||||
|
// TODO: Move to the client
|
||||||
public int DisplaySortOrder { get; protected init; }
|
public int DisplaySortOrder { get; protected init; }
|
||||||
|
// TODO: Remove
|
||||||
public int? LegacyYear { get; protected init; }
|
public int? LegacyYear { get; protected init; }
|
||||||
public bool Disabled { get; protected init; }
|
public bool Disabled { get; protected init; }
|
||||||
public PasswordManagerPlanFeatures PasswordManager { get; protected init; }
|
public PasswordManagerPlanFeatures PasswordManager { get; protected init; }
|
||||||
@ -45,15 +50,19 @@ public abstract record Plan
|
|||||||
public string StripeServiceAccountPlanId { get; init; }
|
public string StripeServiceAccountPlanId { get; init; }
|
||||||
public decimal? AdditionalPricePerServiceAccount { get; init; }
|
public decimal? AdditionalPricePerServiceAccount { get; init; }
|
||||||
public short BaseServiceAccount { get; init; }
|
public short BaseServiceAccount { get; init; }
|
||||||
|
// TODO: Unused, remove
|
||||||
public short? MaxAdditionalServiceAccount { get; init; }
|
public short? MaxAdditionalServiceAccount { get; init; }
|
||||||
public bool HasAdditionalServiceAccountOption { get; init; }
|
public bool HasAdditionalServiceAccountOption { get; init; }
|
||||||
// Seats
|
// Seats
|
||||||
public string StripeSeatPlanId { get; init; }
|
public string StripeSeatPlanId { get; init; }
|
||||||
public bool HasAdditionalSeatsOption { get; init; }
|
public bool HasAdditionalSeatsOption { get; init; }
|
||||||
|
// TODO: Remove, SM is never packaged
|
||||||
public decimal BasePrice { get; init; }
|
public decimal BasePrice { get; init; }
|
||||||
public decimal SeatPrice { get; init; }
|
public decimal SeatPrice { get; init; }
|
||||||
|
// TODO: Remove, SM is never packaged
|
||||||
public int BaseSeats { get; init; }
|
public int BaseSeats { get; init; }
|
||||||
public short? MaxSeats { get; init; }
|
public short? MaxSeats { get; init; }
|
||||||
|
// TODO: Unused, remove
|
||||||
public int? MaxAdditionalSeats { get; init; }
|
public int? MaxAdditionalSeats { get; init; }
|
||||||
public bool AllowSeatAutoscale { get; init; }
|
public bool AllowSeatAutoscale { get; init; }
|
||||||
|
|
||||||
@ -72,8 +81,10 @@ public abstract record Plan
|
|||||||
public decimal ProviderPortalSeatPrice { get; init; }
|
public decimal ProviderPortalSeatPrice { get; init; }
|
||||||
public bool AllowSeatAutoscale { get; init; }
|
public bool AllowSeatAutoscale { get; init; }
|
||||||
public bool HasAdditionalSeatsOption { get; init; }
|
public bool HasAdditionalSeatsOption { get; init; }
|
||||||
|
// TODO: Remove, never set.
|
||||||
public int? MaxAdditionalSeats { get; init; }
|
public int? MaxAdditionalSeats { get; init; }
|
||||||
public int BaseSeats { get; init; }
|
public int BaseSeats { get; init; }
|
||||||
|
// TODO: Remove premium access as it's deprecated
|
||||||
public bool HasPremiumAccessOption { get; init; }
|
public bool HasPremiumAccessOption { get; init; }
|
||||||
public string StripePremiumAccessPlanId { get; init; }
|
public string StripePremiumAccessPlanId { get; init; }
|
||||||
public decimal PremiumAccessOptionPrice { get; init; }
|
public decimal PremiumAccessOptionPrice { get; init; }
|
||||||
@ -83,6 +94,7 @@ public abstract record Plan
|
|||||||
public bool HasAdditionalStorageOption { get; init; }
|
public bool HasAdditionalStorageOption { get; init; }
|
||||||
public decimal AdditionalStoragePricePerGb { get; init; }
|
public decimal AdditionalStoragePricePerGb { get; init; }
|
||||||
public string StripeStoragePlanId { get; init; }
|
public string StripeStoragePlanId { get; init; }
|
||||||
|
// TODO: Remove
|
||||||
public short? MaxAdditionalStorage { get; init; }
|
public short? MaxAdditionalStorage { get; init; }
|
||||||
// Feature
|
// Feature
|
||||||
public short? MaxCollections { get; init; }
|
public short? MaxCollections { get; init; }
|
||||||
|
12
src/Core/Billing/Pricing/IPricingClient.cs
Normal file
12
src/Core/Billing/Pricing/IPricingClient.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Models.StaticStore;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Pricing;
|
||||||
|
|
||||||
|
public interface IPricingClient
|
||||||
|
{
|
||||||
|
Task<Plan?> GetPlan(PlanType planType);
|
||||||
|
Task<List<Plan>> ListPlans();
|
||||||
|
}
|
232
src/Core/Billing/Pricing/PlanAdapter.cs
Normal file
232
src/Core/Billing/Pricing/PlanAdapter.cs
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Models.StaticStore;
|
||||||
|
using Proto.Billing.Pricing;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Pricing;
|
||||||
|
|
||||||
|
public record PlanAdapter : Plan
|
||||||
|
{
|
||||||
|
public PlanAdapter(PlanResponse planResponse)
|
||||||
|
{
|
||||||
|
Type = ToPlanType(planResponse.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;
|
||||||
|
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 = planResponse.AdditionalData != null
|
||||||
|
? int.Parse(planResponse.AdditionalData["upgradeSortOrder"])
|
||||||
|
: 0;
|
||||||
|
DisplaySortOrder = planResponse.AdditionalData != null
|
||||||
|
? int.Parse(planResponse.AdditionalData["displaySortOrder"])
|
||||||
|
: 0;
|
||||||
|
HasCustomPermissions = HasFeature("customPermissions");
|
||||||
|
Disabled = !planResponse.Available;
|
||||||
|
PasswordManager = ToPasswordManagerPlanFeatures(planResponse);
|
||||||
|
SecretsManager = planResponse.SecretsManager != null ? ToSecretsManagerPlanFeatures(planResponse) : null;
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool HasFeature(string lookupKey) => planResponse.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(PlanResponse planResponse)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
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,
|
||||||
|
StripeStoragePlanId = stripeStoragePlanId,
|
||||||
|
MaxCollections = maxCollections
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(PlanResponse planResponse)
|
||||||
|
{
|
||||||
|
var seats = planResponse.SecretsManager.Seats;
|
||||||
|
var serviceAccounts = planResponse.SecretsManager.ServiceAccounts;
|
||||||
|
|
||||||
|
var maxServiceAccounts = GetMaxServiceAccounts(serviceAccounts);
|
||||||
|
var allowServiceAccountsAutoscale = serviceAccounts.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
|
||||||
|
var stripeServiceAccountPlanId = GetStripeServiceAccountPlanId(serviceAccounts);
|
||||||
|
var additionalPricePerServiceAccount = GetAdditionalPricePerServiceAccount(serviceAccounts);
|
||||||
|
var baseServiceAccount = GetBaseServiceAccount(serviceAccounts);
|
||||||
|
var hasAdditionalServiceAccountOption = serviceAccounts.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
|
||||||
|
var stripeSeatPlanId = GetStripeSeatPlanId(seats);
|
||||||
|
var hasAdditionalSeatsOption = seats.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
|
||||||
|
? null
|
||||||
|
: decimal.Parse(freeOrScalable.Scalable.Price);
|
||||||
|
|
||||||
|
private static decimal GetBasePrice(PurchasableDTO purchasable)
|
||||||
|
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? 0 : decimal.Parse(purchasable.Packaged.Price);
|
||||||
|
|
||||||
|
private static int GetBaseSeats(PurchasableDTO purchasable)
|
||||||
|
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? 0 : purchasable.Packaged.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
|
||||||
|
};
|
||||||
|
|
||||||
|
private static short? GetMaxSeats(PurchasableDTO purchasable)
|
||||||
|
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Free ? null : (short)purchasable.Free.Quantity;
|
||||||
|
|
||||||
|
private static short? GetMaxSeats(FreeOrScalableDTO freeOrScalable)
|
||||||
|
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Free ? null : (short)freeOrScalable.Free.Quantity;
|
||||||
|
|
||||||
|
private static short? GetMaxServiceAccounts(FreeOrScalableDTO freeOrScalable)
|
||||||
|
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Free ? null : (short)freeOrScalable.Free.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
|
||||||
|
};
|
||||||
|
|
||||||
|
private static decimal GetSeatPrice(FreeOrScalableDTO freeOrScalable)
|
||||||
|
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
|
||||||
|
? 0
|
||||||
|
: decimal.Parse(freeOrScalable.Scalable.Price);
|
||||||
|
|
||||||
|
private static string? GetStripePlanId(PurchasableDTO purchasable)
|
||||||
|
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? null : purchasable.Packaged.StripePriceId;
|
||||||
|
|
||||||
|
private static string? GetStripeSeatPlanId(PurchasableDTO purchasable)
|
||||||
|
=> purchasable.KindCase switch
|
||||||
|
{
|
||||||
|
PurchasableDTO.KindOneofCase.Packaged => purchasable.Packaged.Additional?.StripePriceId,
|
||||||
|
PurchasableDTO.KindOneofCase.Scalable => purchasable.Scalable.StripePriceId,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string? GetStripeSeatPlanId(FreeOrScalableDTO freeOrScalable)
|
||||||
|
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
|
||||||
|
? null
|
||||||
|
: freeOrScalable.Scalable.StripePriceId;
|
||||||
|
|
||||||
|
private static string? GetStripeServiceAccountPlanId(FreeOrScalableDTO freeOrScalable)
|
||||||
|
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
|
||||||
|
? null
|
||||||
|
: freeOrScalable.Scalable.StripePriceId;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
92
src/Core/Billing/Pricing/PricingClient.cs
Normal file
92
src/Core/Billing/Pricing/PricingClient.cs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Models.StaticStore;
|
||||||
|
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;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Pricing;
|
||||||
|
|
||||||
|
public class PricingClient(
|
||||||
|
IFeatureService featureService,
|
||||||
|
GlobalSettings globalSettings) : IPricingClient
|
||||||
|
{
|
||||||
|
public async Task<Plan?> GetPlan(PlanType planType)
|
||||||
|
{
|
||||||
|
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
||||||
|
|
||||||
|
if (!usePricingService)
|
||||||
|
{
|
||||||
|
return StaticStore.GetPlan(planType);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri);
|
||||||
|
var client = new PasswordManager.PasswordManagerClient(channel);
|
||||||
|
|
||||||
|
var lookupKey = ToLookupKey(planType);
|
||||||
|
if (string.IsNullOrEmpty(lookupKey))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response =
|
||||||
|
await client.GetPlanByLookupKeyAsync(new GetPlanByLookupKeyRequest { LookupKey = lookupKey });
|
||||||
|
|
||||||
|
return new PlanAdapter(response);
|
||||||
|
}
|
||||||
|
catch (RpcException rpcException) when (rpcException.StatusCode == StatusCode.NotFound)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Plan>> ListPlans()
|
||||||
|
{
|
||||||
|
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
||||||
|
|
||||||
|
if (!usePricingService)
|
||||||
|
{
|
||||||
|
return StaticStore.Plans.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri);
|
||||||
|
var client = new PasswordManager.PasswordManagerClient(channel);
|
||||||
|
|
||||||
|
var response = await client.ListPlansAsync(new Empty());
|
||||||
|
return response.Plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ToLookupKey(PlanType planType)
|
||||||
|
=> planType switch
|
||||||
|
{
|
||||||
|
PlanType.EnterpriseAnnually => "enterprise-annually",
|
||||||
|
PlanType.EnterpriseAnnually2019 => "enterprise-annually-2019",
|
||||||
|
PlanType.EnterpriseAnnually2020 => "enterprise-annually-2020",
|
||||||
|
PlanType.EnterpriseAnnually2023 => "enterprise-annually-2023",
|
||||||
|
PlanType.EnterpriseMonthly => "enterprise-monthly",
|
||||||
|
PlanType.EnterpriseMonthly2019 => "enterprise-monthly-2019",
|
||||||
|
PlanType.EnterpriseMonthly2020 => "enterprise-monthly-2020",
|
||||||
|
PlanType.EnterpriseMonthly2023 => "enterprise-monthly-2023",
|
||||||
|
PlanType.FamiliesAnnually => "families",
|
||||||
|
PlanType.FamiliesAnnually2019 => "families-2019",
|
||||||
|
PlanType.Free => "free",
|
||||||
|
PlanType.TeamsAnnually => "teams-annually",
|
||||||
|
PlanType.TeamsAnnually2019 => "teams-annually-2019",
|
||||||
|
PlanType.TeamsAnnually2020 => "teams-annually-2020",
|
||||||
|
PlanType.TeamsAnnually2023 => "teams-annually-2023",
|
||||||
|
PlanType.TeamsMonthly => "teams-monthly",
|
||||||
|
PlanType.TeamsMonthly2019 => "teams-monthly-2019",
|
||||||
|
PlanType.TeamsMonthly2020 => "teams-monthly-2020",
|
||||||
|
PlanType.TeamsMonthly2023 => "teams-monthly-2023",
|
||||||
|
PlanType.TeamsStarter => "teams-starter",
|
||||||
|
PlanType.TeamsStarter2023 => "teams-starter-2023",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
92
src/Core/Billing/Pricing/Protos/password-manager.proto
Normal file
92
src/Core/Billing/Pricing/Protos/password-manager.proto
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
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;
|
||||||
|
}
|
@ -164,6 +164,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string AuthenticatorSyncAndroid = "enable-authenticator-sync-android";
|
public const string AuthenticatorSyncAndroid = "enable-authenticator-sync-android";
|
||||||
public const string AppReviewPrompt = "app-review-prompt";
|
public const string AppReviewPrompt = "app-review-prompt";
|
||||||
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
|
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
|
||||||
|
public const string UsePricingService = "use-pricing-service";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -25,6 +25,12 @@
|
|||||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.64" />
|
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.64" />
|
||||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||||
|
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
|
||||||
|
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
|
||||||
|
<PackageReference Include="Grpc.Tools" Version="2.68.1">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
|
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
|
||||||
@ -44,6 +50,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.10" />
|
||||||
|
<PackageReference Include="OneOf" Version="3.0.271" />
|
||||||
<PackageReference Include="Quartz" Version="3.13.1" />
|
<PackageReference Include="Quartz" Version="3.13.1" />
|
||||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||||
@ -62,6 +69,10 @@
|
|||||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
|
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Protobuf Include="Billing\Pricing\Protos\password-manager.proto" GrpcServices="Client" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Resources\" />
|
<Folder Include="Resources\" />
|
||||||
<Folder Include="Properties\" />
|
<Folder Include="Properties\" />
|
||||||
|
@ -81,8 +81,8 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public virtual IDomainVerificationSettings DomainVerification { get; set; } = new DomainVerificationSettings();
|
public virtual IDomainVerificationSettings DomainVerification { get; set; } = new DomainVerificationSettings();
|
||||||
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
|
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
|
||||||
public virtual string DevelopmentDirectory { get; set; }
|
public virtual string DevelopmentDirectory { get; set; }
|
||||||
|
|
||||||
public virtual bool EnableEmailVerification { get; set; }
|
public virtual bool EnableEmailVerification { get; set; }
|
||||||
|
public virtual string PricingUri { get; set; }
|
||||||
|
|
||||||
public string BuildExternalUri(string explicitValue, string name)
|
public string BuildExternalUri(string explicitValue, string name)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user