mirror of
https://github.com/bitwarden/server
synced 2025-12-15 07:43:54 +00:00
192 lines
7.1 KiB
C#
192 lines
7.1 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using Bit.Core.Billing.Constants;
|
|
using Bit.Core.Billing.Enums;
|
|
using Bit.Core.Billing.Pricing.Organizations;
|
|
using Bit.Core.Exceptions;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Settings;
|
|
using Bit.Core.Utilities;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Bit.Core.Billing.Pricing;
|
|
|
|
using OrganizationPlan = Bit.Core.Models.StaticStore.Plan;
|
|
using PremiumPlan = Premium.Plan;
|
|
using Purchasable = Premium.Purchasable;
|
|
|
|
public class PricingClient(
|
|
IFeatureService featureService,
|
|
GlobalSettings globalSettings,
|
|
HttpClient httpClient,
|
|
ILogger<PricingClient> logger) : IPricingClient
|
|
{
|
|
public async Task<OrganizationPlan?> GetPlan(PlanType planType)
|
|
{
|
|
if (globalSettings.SelfHosted)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
|
|
|
if (!usePricingService)
|
|
{
|
|
return StaticStore.GetPlan(planType);
|
|
}
|
|
|
|
var lookupKey = GetLookupKey(planType);
|
|
|
|
if (lookupKey == null)
|
|
{
|
|
logger.LogError("Could not find Pricing Service lookup key for PlanType {PlanType}", planType);
|
|
return null;
|
|
}
|
|
|
|
var response = await httpClient.GetAsync($"plans/organization/{lookupKey}");
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var plan = await response.Content.ReadFromJsonAsync<Plan>();
|
|
return plan == null
|
|
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
|
|
: new PlanAdapter(PreProcessFamiliesPreMigrationPlan(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<OrganizationPlan> GetPlanOrThrow(PlanType planType)
|
|
{
|
|
var plan = await GetPlan(planType);
|
|
|
|
return plan ?? throw new NotFoundException($"Could not find plan for type {planType}");
|
|
}
|
|
|
|
public async Task<List<OrganizationPlan>> ListPlans()
|
|
{
|
|
if (globalSettings.SelfHosted)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
|
|
|
if (!usePricingService)
|
|
{
|
|
return StaticStore.Plans.ToList();
|
|
}
|
|
|
|
var response = await httpClient.GetAsync("plans/organization");
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var plans = await response.Content.ReadFromJsonAsync<List<Plan>>();
|
|
return plans == null
|
|
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
|
|
: plans.Select(OrganizationPlan (plan) => new PlanAdapter(PreProcessFamiliesPreMigrationPlan(plan))).ToList();
|
|
}
|
|
|
|
throw new BillingException(
|
|
message: $"Request to the Pricing Service failed with status {response.StatusCode}");
|
|
}
|
|
|
|
public async Task<PremiumPlan> GetAvailablePremiumPlan()
|
|
{
|
|
var premiumPlans = await ListPremiumPlans();
|
|
|
|
var availablePlan = premiumPlans.FirstOrDefault(premiumPlan => premiumPlan.Available);
|
|
|
|
return availablePlan ?? throw new NotFoundException("Could not find available premium plan");
|
|
}
|
|
|
|
public async Task<List<PremiumPlan>> ListPremiumPlans()
|
|
{
|
|
if (globalSettings.SelfHosted)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
|
var fetchPremiumPriceFromPricingService =
|
|
featureService.IsEnabled(FeatureFlagKeys.PM26793_FetchPremiumPriceFromPricingService);
|
|
|
|
if (!usePricingService || !fetchPremiumPriceFromPricingService)
|
|
{
|
|
return [CurrentPremiumPlan];
|
|
}
|
|
|
|
var response = await httpClient.GetAsync("plans/premium");
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var plans = await response.Content.ReadFromJsonAsync<List<PremiumPlan>>();
|
|
return plans ?? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
|
}
|
|
|
|
throw new BillingException(
|
|
message: $"Request to the Pricing Service failed with status {response.StatusCode}");
|
|
}
|
|
|
|
private string? GetLookupKey(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.FamiliesAnnually2025 =>
|
|
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3)
|
|
? "families-2025"
|
|
: "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
|
|
};
|
|
|
|
/// <summary>
|
|
/// Safeguard used until the feature flag is enabled. Pricing service will return the
|
|
/// 2025PreMigration plan with "families" lookup key. When that is detected and the FF
|
|
/// is still disabled, set the lookup key to families-2025 so PlanAdapter will assign
|
|
/// the correct plan.
|
|
/// </summary>
|
|
/// <param name="plan">The plan to preprocess</param>
|
|
private Plan PreProcessFamiliesPreMigrationPlan(Plan plan)
|
|
{
|
|
if (plan.LookupKey == "families" && !featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3))
|
|
plan.LookupKey = "families-2025";
|
|
return plan;
|
|
}
|
|
|
|
private static PremiumPlan CurrentPremiumPlan => new()
|
|
{
|
|
Name = "Premium",
|
|
Available = true,
|
|
LegacyYear = null,
|
|
Seat = new Purchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
|
|
Storage = new Purchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal, Provided = 1 }
|
|
};
|
|
}
|