1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-30 13:33:24 +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:
Alex Morask 2024-06-24 11:16:57 -04:00 committed by GitHub
parent fa62b36d44
commit 95f54b616e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 28 additions and 14 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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