mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-26793] Fetch premium plan from pricing service (#6450)
* Fetch premium plan from pricing service * Run dotnet format
This commit is contained in:
@@ -3,7 +3,7 @@ using Bit.Core.Billing.Pricing;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
[Route("plans")]
|
||||
[Authorize("Web")]
|
||||
@@ -18,4 +18,11 @@ public class PlansController(
|
||||
var responses = plans.Select(plan => new PlanResponseModel(plan));
|
||||
return new ListResponseModel<PlanResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("premium")]
|
||||
public async Task<IResult> GetPremiumPlanAsync()
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
return TypedResults.Ok(premiumPlan);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -50,7 +51,8 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
ISubscriberService subscriberService,
|
||||
IUserService userService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ILogger<CreatePremiumCloudHostedSubscriptionCommand> logger)
|
||||
ILogger<CreatePremiumCloudHostedSubscriptionCommand> logger,
|
||||
IPricingClient pricingClient)
|
||||
: BaseBillingCommand<CreatePremiumCloudHostedSubscriptionCommand>(logger), ICreatePremiumCloudHostedSubscriptionCommand
|
||||
{
|
||||
private static readonly List<string> _expand = ["tax"];
|
||||
@@ -255,11 +257,13 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
Customer customer,
|
||||
int? storage)
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Price = StripeConstants.Prices.PremiumAnnually,
|
||||
Price = premiumPlan.Seat.StripePriceId,
|
||||
Quantity = 1
|
||||
}
|
||||
};
|
||||
@@ -268,7 +272,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = StripeConstants.Prices.StoragePlanPersonal,
|
||||
Price = premiumPlan.Storage.StripePriceId,
|
||||
Quantity = storage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Premium.Commands;
|
||||
|
||||
using static StripeConstants;
|
||||
|
||||
public interface IPreviewPremiumTaxCommand
|
||||
{
|
||||
Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(
|
||||
@@ -18,6 +16,7 @@ public interface IPreviewPremiumTaxCommand
|
||||
|
||||
public class PreviewPremiumTaxCommand(
|
||||
ILogger<PreviewPremiumTaxCommand> logger,
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter) : BaseBillingCommand<PreviewPremiumTaxCommand>(logger), IPreviewPremiumTaxCommand
|
||||
{
|
||||
public Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(
|
||||
@@ -25,6 +24,8 @@ public class PreviewPremiumTaxCommand(
|
||||
BillingAddress billingAddress)
|
||||
=> HandleAsync<(decimal, decimal)>(async () =>
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true },
|
||||
@@ -41,7 +42,7 @@ public class PreviewPremiumTaxCommand(
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new InvoiceSubscriptionDetailsItemOptions { Price = Prices.PremiumAnnually, Quantity = 1 }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Price = premiumPlan.Seat.StripePriceId, Quantity = 1 }
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -50,7 +51,7 @@ public class PreviewPremiumTaxCommand(
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Price = Prices.StoragePlanPersonal,
|
||||
Price = premiumPlan.Storage.StripePriceId,
|
||||
Quantity = additionalStorage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Billing.Pricing;
|
||||
|
||||
using OrganizationPlan = Plan;
|
||||
using PremiumPlan = Premium.Plan;
|
||||
|
||||
public interface IPricingClient
|
||||
{
|
||||
// TODO: Rename with Organization focus.
|
||||
/// <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"/>.
|
||||
@@ -16,8 +18,9 @@ public interface IPricingClient
|
||||
/// <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);
|
||||
Task<OrganizationPlan?> GetPlan(PlanType planType);
|
||||
|
||||
// TODO: Rename with Organization focus.
|
||||
/// <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"/>.
|
||||
@@ -26,13 +29,17 @@ public interface IPricingClient
|
||||
/// <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);
|
||||
Task<OrganizationPlan> GetPlanOrThrow(PlanType planType);
|
||||
|
||||
// TODO: Rename with Organization focus.
|
||||
/// <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();
|
||||
Task<List<OrganizationPlan>> ListPlans();
|
||||
|
||||
Task<PremiumPlan> GetAvailablePremiumPlan();
|
||||
Task<List<PremiumPlan>> ListPremiumPlans();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
public class Feature
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
public class Plan
|
||||
{
|
||||
@@ -1,8 +1,6 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing.Models;
|
||||
using Plan = Bit.Core.Billing.Pricing.Models.Plan;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
public record PlanAdapter : Core.Models.StaticStore.Plan
|
||||
{
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using OneOf;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
namespace Bit.Core.Billing.Pricing.Organizations;
|
||||
|
||||
[JsonConverter(typeof(PurchasableJsonConverter))]
|
||||
public class Purchasable(OneOf<Free, Packaged, Scalable> input) : OneOfBase<Free, Packaged, Scalable>(input)
|
||||
10
src/Core/Billing/Pricing/Premium/Plan.cs
Normal file
10
src/Core/Billing/Pricing/Premium/Plan.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.Billing.Pricing.Premium;
|
||||
|
||||
public class Plan
|
||||
{
|
||||
public string Name { get; init; } = null!;
|
||||
public int? LegacyYear { get; init; }
|
||||
public bool Available { get; init; }
|
||||
public Purchasable Seat { get; init; } = null!;
|
||||
public Purchasable Storage { get; init; } = null!;
|
||||
}
|
||||
7
src/Core/Billing/Pricing/Premium/Purchasable.cs
Normal file
7
src/Core/Billing/Pricing/Premium/Purchasable.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Billing.Pricing.Premium;
|
||||
|
||||
public class Purchasable
|
||||
{
|
||||
public string StripePriceId { get; init; } = null!;
|
||||
public decimal Price { get; init; }
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
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;
|
||||
using Plan = Bit.Core.Models.StaticStore.Plan;
|
||||
|
||||
#nullable enable
|
||||
|
||||
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<Plan?> GetPlan(PlanType planType)
|
||||
public async Task<OrganizationPlan?> GetPlan(PlanType planType)
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
@@ -40,16 +43,14 @@ public class PricingClient(
|
||||
return null;
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync($"plans/lookup/{lookupKey}");
|
||||
var response = await httpClient.GetAsync($"plans/organization/{lookupKey}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var plan = await response.Content.ReadFromJsonAsync<Models.Plan>();
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
||||
}
|
||||
return new PlanAdapter(plan);
|
||||
var plan = await response.Content.ReadFromJsonAsync<Plan>();
|
||||
return plan == null
|
||||
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
|
||||
: new PlanAdapter(plan);
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
@@ -62,19 +63,14 @@ public class PricingClient(
|
||||
message: $"Request to the Pricing Service failed with status code {response.StatusCode}");
|
||||
}
|
||||
|
||||
public async Task<Plan> GetPlanOrThrow(PlanType planType)
|
||||
public async Task<OrganizationPlan> GetPlanOrThrow(PlanType planType)
|
||||
{
|
||||
var plan = await GetPlan(planType);
|
||||
|
||||
if (plan == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return plan;
|
||||
return plan ?? throw new NotFoundException($"Could not find plan for type {planType}");
|
||||
}
|
||||
|
||||
public async Task<List<Plan>> ListPlans()
|
||||
public async Task<List<OrganizationPlan>> ListPlans()
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
@@ -88,16 +84,51 @@ public class PricingClient(
|
||||
return StaticStore.Plans.ToList();
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync("plans");
|
||||
var response = await httpClient.GetAsync("plans/organization");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var plans = await response.Content.ReadFromJsonAsync<List<Models.Plan>>();
|
||||
if (plans == null)
|
||||
{
|
||||
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
||||
}
|
||||
return plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList();
|
||||
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(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(
|
||||
@@ -130,4 +161,13 @@ public class PricingClient(
|
||||
PlanType.TeamsStarter2023 => "teams-starter-2023",
|
||||
_ => null
|
||||
};
|
||||
|
||||
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 }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -30,7 +31,8 @@ public class PremiumUserBillingService(
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService,
|
||||
IUserRepository userRepository) : IPremiumUserBillingService
|
||||
IUserRepository userRepository,
|
||||
IPricingClient pricingClient) : IPremiumUserBillingService
|
||||
{
|
||||
public async Task Credit(User user, decimal amount)
|
||||
{
|
||||
@@ -301,11 +303,13 @@ public class PremiumUserBillingService(
|
||||
Customer customer,
|
||||
int? storage)
|
||||
{
|
||||
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Price = StripeConstants.Prices.PremiumAnnually,
|
||||
Price = premiumPlan.Seat.StripePriceId,
|
||||
Quantity = 1
|
||||
}
|
||||
};
|
||||
@@ -314,7 +318,7 @@ public class PremiumUserBillingService(
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = StripeConstants.Prices.StoragePlanPersonal,
|
||||
Price = premiumPlan.Storage.StripePriceId,
|
||||
Quantity = storage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button";
|
||||
public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog";
|
||||
public const string PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page";
|
||||
public const string PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service";
|
||||
|
||||
/* Key Management Team */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Responses;
|
||||
@@ -896,11 +897,14 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete($"Use {nameof(PreviewPremiumTaxCommand)} instead.")]
|
||||
public async Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(
|
||||
PreviewIndividualInvoiceRequestBody parameters,
|
||||
string gatewayCustomerId,
|
||||
string gatewaySubscriptionId)
|
||||
{
|
||||
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true, },
|
||||
@@ -909,8 +913,17 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = StripeConstants.Prices.PremiumAnnually },
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.AdditionalStorage, Plan = StripeConstants.Prices.StoragePlanPersonal }
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = 1,
|
||||
Plan = premiumPlan.Seat.StripePriceId
|
||||
},
|
||||
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.PasswordManager.AdditionalStorage,
|
||||
Plan = premiumPlan.Storage.StripePriceId
|
||||
}
|
||||
]
|
||||
},
|
||||
CustomerDetails = new InvoiceCustomerDetailsOptions
|
||||
@@ -1028,7 +1041,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new()
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.PasswordManager.AdditionalStorage,
|
||||
Plan = plan.PasswordManager.StripeStoragePlanId
|
||||
@@ -1049,7 +1062,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(parameters.PasswordManager.SponsoredPlan.Value);
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new() { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
|
||||
);
|
||||
}
|
||||
else
|
||||
@@ -1057,13 +1070,13 @@ public class StripePaymentService : IPaymentService
|
||||
if (plan.PasswordManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new() { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new() { Quantity = 1, Plan = plan.PasswordManager.StripePlanId }
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = plan.PasswordManager.StripePlanId }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1071,7 +1084,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
if (plan.SecretsManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new()
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.SecretsManager?.Seats ?? 0,
|
||||
Plan = plan.SecretsManager.StripeSeatPlanId
|
||||
@@ -1080,7 +1093,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
if (plan.SecretsManager.HasAdditionalServiceAccountOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new()
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0,
|
||||
Plan = plan.SecretsManager.StripeServiceAccountPlanId
|
||||
|
||||
@@ -14,10 +14,10 @@ using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
@@ -72,6 +72,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IDistributedCache _distributedCache;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public UserService(
|
||||
IUserRepository userRepository,
|
||||
@@ -106,7 +107,8 @@ public class UserService : UserManager<User>, IUserService
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IDistributedCache distributedCache,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IPricingClient pricingClient)
|
||||
: base(
|
||||
store,
|
||||
optionsAccessor,
|
||||
@@ -146,6 +148,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_distributedCache = distributedCache;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
||||
@@ -972,8 +975,9 @@ public class UserService : UserManager<User>, IUserService
|
||||
throw new BadRequestException("Not a premium user.");
|
||||
}
|
||||
|
||||
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb,
|
||||
StripeConstants.Prices.StoragePlanPersonal);
|
||||
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, premiumPlan.Storage.StripePriceId);
|
||||
await SaveUserAsync(user);
|
||||
return secret;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Platform.Push;
|
||||
@@ -14,6 +16,8 @@ using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
using Address = Stripe.Address;
|
||||
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
|
||||
using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;
|
||||
using StripeCustomer = Stripe.Customer;
|
||||
using StripeSubscription = Stripe.Subscription;
|
||||
|
||||
@@ -28,6 +32,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
|
||||
private readonly IUserService _userService = Substitute.For<IUserService>();
|
||||
private readonly IPushNotificationService _pushNotificationService = Substitute.For<IPushNotificationService>();
|
||||
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||
private readonly CreatePremiumCloudHostedSubscriptionCommand _command;
|
||||
|
||||
public CreatePremiumCloudHostedSubscriptionCommandTests()
|
||||
@@ -36,6 +41,17 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
baseServiceUri.CloudRegion.Returns("US");
|
||||
_globalSettings.BaseServiceUri.Returns(baseServiceUri);
|
||||
|
||||
// Setup default premium plan with standard pricing
|
||||
var premiumPlan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
|
||||
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal }
|
||||
};
|
||||
_pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
|
||||
|
||||
_command = new CreatePremiumCloudHostedSubscriptionCommand(
|
||||
_braintreeGateway,
|
||||
_globalSettings,
|
||||
@@ -44,7 +60,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
_subscriberService,
|
||||
_userService,
|
||||
_pushNotificationService,
|
||||
Substitute.For<ILogger<CreatePremiumCloudHostedSubscriptionCommand>>());
|
||||
Substitute.For<ILogger<CreatePremiumCloudHostedSubscriptionCommand>>(),
|
||||
_pricingClient);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
using static Bit.Core.Billing.Constants.StripeConstants;
|
||||
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
|
||||
using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Premium.Commands;
|
||||
|
||||
public class PreviewPremiumTaxCommandTests
|
||||
{
|
||||
private readonly ILogger<PreviewPremiumTaxCommand> _logger = Substitute.For<ILogger<PreviewPremiumTaxCommand>>();
|
||||
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly PreviewPremiumTaxCommand _command;
|
||||
|
||||
public PreviewPremiumTaxCommandTests()
|
||||
{
|
||||
_command = new PreviewPremiumTaxCommand(_logger, _stripeAdapter);
|
||||
// Setup default premium plan with standard pricing
|
||||
var premiumPlan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = Prices.PremiumAnnually },
|
||||
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }
|
||||
};
|
||||
_pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
|
||||
|
||||
_command = new PreviewPremiumTaxCommand(_logger, _pricingClient, _stripeAdapter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user