mirror of
https://github.com/bitwarden/server.git
synced 2024-11-28 13:15:12 +01:00
[AC-2744] Add provider portal pricing for consolidated billing (#4210)
* Expanded Teams and Enterprise plan with provider seat data * Updated provider setup process with new plan information * Updated provider subscription retrieval and update with new plan information * Updated client invoice report with new plan information * Fixed tests * Fix broken test
This commit is contained in:
parent
fa62b36d44
commit
95f54b616e
@ -450,7 +450,7 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = teamsPlan.PasswordManager.StripeSeatPlanId,
|
Price = teamsPlan.PasswordManager.StripeProviderPortalSeatPlanId,
|
||||||
Quantity = teamsProviderPlan.SeatMinimum
|
Quantity = teamsProviderPlan.SeatMinimum
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -468,7 +468,7 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = enterprisePlan.PasswordManager.StripeSeatPlanId,
|
Price = enterprisePlan.PasswordManager.StripeProviderPortalSeatPlanId,
|
||||||
Quantity = enterpriseProviderPlan.SeatMinimum
|
Quantity = enterpriseProviderPlan.SeatMinimum
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1083,9 +1083,9 @@ public class ProviderBillingServiceTests
|
|||||||
sub.Customer == "customer_id" &&
|
sub.Customer == "customer_id" &&
|
||||||
sub.DaysUntilDue == 30 &&
|
sub.DaysUntilDue == 30 &&
|
||||||
sub.Items.Count == 2 &&
|
sub.Items.Count == 2 &&
|
||||||
sub.Items.ElementAt(0).Price == teamsPlan.PasswordManager.StripeSeatPlanId &&
|
sub.Items.ElementAt(0).Price == teamsPlan.PasswordManager.StripeProviderPortalSeatPlanId &&
|
||||||
sub.Items.ElementAt(0).Quantity == 100 &&
|
sub.Items.ElementAt(0).Quantity == 100 &&
|
||||||
sub.Items.ElementAt(1).Price == enterprisePlan.PasswordManager.StripeSeatPlanId &&
|
sub.Items.ElementAt(1).Price == enterprisePlan.PasswordManager.StripeProviderPortalSeatPlanId &&
|
||||||
sub.Items.ElementAt(1).Quantity == 100 &&
|
sub.Items.ElementAt(1).Quantity == 100 &&
|
||||||
sub.Metadata["providerId"] == provider.Id.ToString() &&
|
sub.Metadata["providerId"] == provider.Id.ToString() &&
|
||||||
sub.OffSession == true &&
|
sub.OffSession == true &&
|
||||||
|
@ -26,7 +26,7 @@ public record ConsolidatedBillingSubscriptionResponse(
|
|||||||
.Select(providerPlan =>
|
.Select(providerPlan =>
|
||||||
{
|
{
|
||||||
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||||
var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.SeatPrice;
|
var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice;
|
||||||
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
|
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
|
||||||
return new ProviderPlanResponse(
|
return new ProviderPlanResponse(
|
||||||
plan.Name,
|
plan.Name,
|
||||||
@ -36,7 +36,9 @@ public record ConsolidatedBillingSubscriptionResponse(
|
|||||||
cost,
|
cost,
|
||||||
cadence);
|
cadence);
|
||||||
});
|
});
|
||||||
|
|
||||||
var gracePeriod = subscription.CollectionMethod == "charge_automatically" ? 14 : 30;
|
var gracePeriod = subscription.CollectionMethod == "charge_automatically" ? 14 : 30;
|
||||||
|
|
||||||
return new ConsolidatedBillingSubscriptionResponse(
|
return new ConsolidatedBillingSubscriptionResponse(
|
||||||
subscription.Status,
|
subscription.Status,
|
||||||
subscription.CurrentPeriodEnd,
|
subscription.CurrentPeriodEnd,
|
||||||
|
@ -121,8 +121,10 @@ public class PlanResponseModel : ResponseModel
|
|||||||
{
|
{
|
||||||
StripePlanId = plan.StripePlanId;
|
StripePlanId = plan.StripePlanId;
|
||||||
StripeSeatPlanId = plan.StripeSeatPlanId;
|
StripeSeatPlanId = plan.StripeSeatPlanId;
|
||||||
|
StripeProviderPortalSeatPlanId = plan.StripeProviderPortalSeatPlanId;
|
||||||
BasePrice = plan.BasePrice;
|
BasePrice = plan.BasePrice;
|
||||||
SeatPrice = plan.SeatPrice;
|
SeatPrice = plan.SeatPrice;
|
||||||
|
ProviderPortalSeatPrice = plan.ProviderPortalSeatPrice;
|
||||||
AllowSeatAutoscale = plan.AllowSeatAutoscale;
|
AllowSeatAutoscale = plan.AllowSeatAutoscale;
|
||||||
HasAdditionalSeatsOption = plan.HasAdditionalSeatsOption;
|
HasAdditionalSeatsOption = plan.HasAdditionalSeatsOption;
|
||||||
MaxAdditionalSeats = plan.MaxAdditionalSeats;
|
MaxAdditionalSeats = plan.MaxAdditionalSeats;
|
||||||
@ -141,8 +143,10 @@ public class PlanResponseModel : ResponseModel
|
|||||||
// Seats
|
// Seats
|
||||||
public string StripePlanId { get; init; }
|
public string StripePlanId { get; init; }
|
||||||
public string StripeSeatPlanId { get; init; }
|
public string StripeSeatPlanId { get; init; }
|
||||||
|
public string StripeProviderPortalSeatPlanId { get; init; }
|
||||||
public decimal BasePrice { get; init; }
|
public decimal BasePrice { get; init; }
|
||||||
public decimal SeatPrice { get; init; }
|
public decimal SeatPrice { get; init; }
|
||||||
|
public decimal ProviderPortalSeatPrice { get; init; }
|
||||||
public bool AllowSeatAutoscale { get; init; }
|
public bool AllowSeatAutoscale { get; init; }
|
||||||
public bool HasAdditionalSeatsOption { get; init; }
|
public bool HasAdditionalSeatsOption { get; init; }
|
||||||
public int? MaxAdditionalSeats { get; init; }
|
public int? MaxAdditionalSeats { get; init; }
|
||||||
|
@ -67,9 +67,9 @@ public class ProviderEventService(
|
|||||||
|
|
||||||
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
|
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
|
||||||
|
|
||||||
var discountedEnterpriseSeatPrice = enterprisePlan.PasswordManager.SeatPrice * discountedPercentage;
|
var discountedEnterpriseSeatPrice = enterprisePlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
|
||||||
|
|
||||||
var discountedTeamsSeatPrice = teamsPlan.PasswordManager.SeatPrice * discountedPercentage;
|
var discountedTeamsSeatPrice = teamsPlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
|
||||||
|
|
||||||
var invoiceItems = clients.Select(client => new ProviderInvoiceItem
|
var invoiceItems = clients.Select(client => new ProviderInvoiceItem
|
||||||
{
|
{
|
||||||
|
@ -63,8 +63,10 @@ public abstract record Plan
|
|||||||
// Seats
|
// Seats
|
||||||
public string StripePlanId { get; init; }
|
public string StripePlanId { get; init; }
|
||||||
public string StripeSeatPlanId { get; init; }
|
public string StripeSeatPlanId { get; init; }
|
||||||
|
public string StripeProviderPortalSeatPlanId { get; init; }
|
||||||
public decimal BasePrice { get; init; }
|
public decimal BasePrice { get; init; }
|
||||||
public decimal SeatPrice { get; init; }
|
public decimal SeatPrice { get; init; }
|
||||||
|
public decimal ProviderPortalSeatPrice { get; init; }
|
||||||
public bool AllowSeatAutoscale { get; init; }
|
public bool AllowSeatAutoscale { get; init; }
|
||||||
public bool HasAdditionalSeatsOption { get; init; }
|
public bool HasAdditionalSeatsOption { get; init; }
|
||||||
public int? MaxAdditionalSeats { get; init; }
|
public int? MaxAdditionalSeats { get; init; }
|
||||||
|
@ -92,8 +92,10 @@ public record EnterprisePlan : Plan
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
StripeSeatPlanId = "2023-enterprise-seat-monthly";
|
StripeSeatPlanId = "2023-enterprise-seat-monthly";
|
||||||
|
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-monthly-2024";
|
||||||
StripeStoragePlanId = "storage-gb-monthly";
|
StripeStoragePlanId = "storage-gb-monthly";
|
||||||
SeatPrice = 7;
|
SeatPrice = 7;
|
||||||
|
ProviderPortalSeatPrice = 6;
|
||||||
AdditionalStoragePricePerGb = 0.5M;
|
AdditionalStoragePricePerGb = 0.5M;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,8 +86,10 @@ public record TeamsPlan : Plan
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
StripeSeatPlanId = "2023-teams-org-seat-monthly";
|
StripeSeatPlanId = "2023-teams-org-seat-monthly";
|
||||||
|
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-teams-monthly-2024";
|
||||||
StripeStoragePlanId = "storage-gb-monthly";
|
StripeStoragePlanId = "storage-gb-monthly";
|
||||||
SeatPrice = 5;
|
SeatPrice = 5;
|
||||||
|
ProviderPortalSeatPrice = 4;
|
||||||
AdditionalStoragePricePerGb = 0.5M;
|
AdditionalStoragePricePerGb = 0.5M;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,9 @@ public class ProviderSubscriptionUpdate : SubscriptionUpdate
|
|||||||
throw ContactSupport($"Cannot create a {nameof(ProviderSubscriptionUpdate)} for {nameof(PlanType)} that doesn't support consolidated billing");
|
throw ContactSupport($"Cannot create a {nameof(ProviderSubscriptionUpdate)} for {nameof(PlanType)} that doesn't support consolidated billing");
|
||||||
}
|
}
|
||||||
|
|
||||||
_planId = GetPasswordManagerPlanId(Utilities.StaticStore.GetPlan(planType));
|
var plan = Utilities.StaticStore.GetPlan(planType);
|
||||||
|
|
||||||
|
_planId = plan.PasswordManager.StripeProviderPortalSeatPlanId;
|
||||||
_previouslyPurchasedSeats = previouslyPurchasedSeats;
|
_previouslyPurchasedSeats = previouslyPurchasedSeats;
|
||||||
_newlyPurchasedSeats = newlyPurchasedSeats;
|
_newlyPurchasedSeats = newlyPurchasedSeats;
|
||||||
}
|
}
|
||||||
|
@ -422,7 +422,7 @@ public class ProviderBillingControllerTests
|
|||||||
Assert.Equal(50, providerTeamsPlan.SeatMinimum);
|
Assert.Equal(50, providerTeamsPlan.SeatMinimum);
|
||||||
Assert.Equal(10, providerTeamsPlan.PurchasedSeats);
|
Assert.Equal(10, providerTeamsPlan.PurchasedSeats);
|
||||||
Assert.Equal(30, providerTeamsPlan.AssignedSeats);
|
Assert.Equal(30, providerTeamsPlan.AssignedSeats);
|
||||||
Assert.Equal(60 * teamsPlan.PasswordManager.SeatPrice, providerTeamsPlan.Cost);
|
Assert.Equal(60 * teamsPlan.PasswordManager.ProviderPortalSeatPrice, providerTeamsPlan.Cost);
|
||||||
Assert.Equal("Monthly", providerTeamsPlan.Cadence);
|
Assert.Equal("Monthly", providerTeamsPlan.Cadence);
|
||||||
|
|
||||||
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||||
@ -431,7 +431,7 @@ public class ProviderBillingControllerTests
|
|||||||
Assert.Equal(100, providerEnterprisePlan.SeatMinimum);
|
Assert.Equal(100, providerEnterprisePlan.SeatMinimum);
|
||||||
Assert.Equal(0, providerEnterprisePlan.PurchasedSeats);
|
Assert.Equal(0, providerEnterprisePlan.PurchasedSeats);
|
||||||
Assert.Equal(90, providerEnterprisePlan.AssignedSeats);
|
Assert.Equal(90, providerEnterprisePlan.AssignedSeats);
|
||||||
Assert.Equal(100 * enterprisePlan.PasswordManager.SeatPrice, providerEnterprisePlan.Cost);
|
Assert.Equal(100 * enterprisePlan.PasswordManager.ProviderPortalSeatPrice, providerEnterprisePlan.Cost);
|
||||||
Assert.Equal("Monthly", providerEnterprisePlan.Cadence);
|
Assert.Equal("Monthly", providerEnterprisePlan.Cadence);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -231,7 +231,7 @@ public class ProviderEventServiceTests
|
|||||||
options.PlanName == "Teams (Monthly)" &&
|
options.PlanName == "Teams (Monthly)" &&
|
||||||
options.AssignedSeats == 50 &&
|
options.AssignedSeats == 50 &&
|
||||||
options.UsedSeats == 30 &&
|
options.UsedSeats == 30 &&
|
||||||
options.Total == options.AssignedSeats * teamsPlan.PasswordManager.SeatPrice * 0.65M));
|
options.Total == options.AssignedSeats * teamsPlan.PasswordManager.ProviderPortalSeatPrice * 0.65M));
|
||||||
|
|
||||||
await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(
|
await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(
|
||||||
options =>
|
options =>
|
||||||
@ -242,7 +242,7 @@ public class ProviderEventServiceTests
|
|||||||
options.PlanName == "Enterprise (Monthly)" &&
|
options.PlanName == "Enterprise (Monthly)" &&
|
||||||
options.AssignedSeats == 50 &&
|
options.AssignedSeats == 50 &&
|
||||||
options.UsedSeats == 30 &&
|
options.UsedSeats == 30 &&
|
||||||
options.Total == options.AssignedSeats * enterprisePlan.PasswordManager.SeatPrice * 0.65M));
|
options.Total == options.AssignedSeats * enterprisePlan.PasswordManager.ProviderPortalSeatPrice * 0.65M));
|
||||||
|
|
||||||
await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(
|
await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(
|
||||||
options =>
|
options =>
|
||||||
@ -253,7 +253,7 @@ public class ProviderEventServiceTests
|
|||||||
options.PlanName == "Teams (Monthly)" &&
|
options.PlanName == "Teams (Monthly)" &&
|
||||||
options.AssignedSeats == 50 &&
|
options.AssignedSeats == 50 &&
|
||||||
options.UsedSeats == 0 &&
|
options.UsedSeats == 0 &&
|
||||||
options.Total == options.AssignedSeats * teamsPlan.PasswordManager.SeatPrice * 0.65M));
|
options.Total == options.AssignedSeats * teamsPlan.PasswordManager.ProviderPortalSeatPrice * 0.65M));
|
||||||
|
|
||||||
await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(
|
await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(
|
||||||
options =>
|
options =>
|
||||||
@ -264,7 +264,7 @@ public class ProviderEventServiceTests
|
|||||||
options.PlanName == "Enterprise (Monthly)" &&
|
options.PlanName == "Enterprise (Monthly)" &&
|
||||||
options.AssignedSeats == 50 &&
|
options.AssignedSeats == 50 &&
|
||||||
options.UsedSeats == 0 &&
|
options.UsedSeats == 0 &&
|
||||||
options.Total == options.AssignedSeats * enterprisePlan.PasswordManager.SeatPrice * 0.65M));
|
options.Total == options.AssignedSeats * enterprisePlan.PasswordManager.ProviderPortalSeatPrice * 0.65M));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
Loading…
Reference in New Issue
Block a user