1
0
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:
Alex Morask
2025-10-22 14:13:16 -05:00
committed by GitHub
parent 0a7e6ae3ca
commit 6a3fc08957
17 changed files with 191 additions and 63 deletions

View File

@@ -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);
}
}

View File

@@ -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
});
}

View File

@@ -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
});
}

View File

@@ -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();
}

View File

@@ -1,4 +1,4 @@
namespace Bit.Core.Billing.Pricing.Models;
namespace Bit.Core.Billing.Pricing.Organizations;
public class Feature
{

View File

@@ -1,4 +1,4 @@
namespace Bit.Core.Billing.Pricing.Models;
namespace Bit.Core.Billing.Pricing.Organizations;
public class Plan
{

View File

@@ -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
{

View File

@@ -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)

View 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!;
}

View 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; }
}

View File

@@ -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 }
};
}

View File

@@ -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
});
}

View File

@@ -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";

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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]

View File

@@ -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]