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