From 95f54b616e6efb4605d0ca01b1f73953f0b02464 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:16:57 -0400 Subject: [PATCH] [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 --- .../src/Commercial.Core/Billing/ProviderBillingService.cs | 4 ++-- .../Billing/ProviderBillingServiceTests.cs | 4 ++-- .../Responses/ConsolidatedBillingSubscriptionResponse.cs | 4 +++- src/Api/Models/Response/PlanResponseModel.cs | 4 ++++ .../Services/Implementations/ProviderEventService.cs | 4 ++-- src/Core/Billing/Models/StaticStore/Plan.cs | 2 ++ .../Billing/Models/StaticStore/Plans/EnterprisePlan.cs | 2 ++ src/Core/Billing/Models/StaticStore/Plans/TeamsPlan.cs | 2 ++ src/Core/Models/Business/ProviderSubscriptionUpdate.cs | 4 +++- .../Billing/Controllers/ProviderBillingControllerTests.cs | 4 ++-- test/Billing.Test/Services/ProviderEventServiceTests.cs | 8 ++++---- 11 files changed, 28 insertions(+), 14 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 422043f04..0fae9e8b2 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -450,7 +450,7 @@ public class ProviderBillingService( subscriptionItemOptionsList.Add(new SubscriptionItemOptions { - Price = teamsPlan.PasswordManager.StripeSeatPlanId, + Price = teamsPlan.PasswordManager.StripeProviderPortalSeatPlanId, Quantity = teamsProviderPlan.SeatMinimum }); @@ -468,7 +468,7 @@ public class ProviderBillingService( subscriptionItemOptionsList.Add(new SubscriptionItemOptions { - Price = enterprisePlan.PasswordManager.StripeSeatPlanId, + Price = enterprisePlan.PasswordManager.StripeProviderPortalSeatPlanId, Quantity = enterpriseProviderPlan.SeatMinimum }); diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs index b5e7ea632..d91553cab 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs @@ -1083,9 +1083,9 @@ public class ProviderBillingServiceTests sub.Customer == "customer_id" && sub.DaysUntilDue == 30 && 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(1).Price == enterprisePlan.PasswordManager.StripeSeatPlanId && + sub.Items.ElementAt(1).Price == enterprisePlan.PasswordManager.StripeProviderPortalSeatPlanId && sub.Items.ElementAt(1).Quantity == 100 && sub.Metadata["providerId"] == provider.Id.ToString() && sub.OffSession == true && diff --git a/src/Api/Billing/Models/Responses/ConsolidatedBillingSubscriptionResponse.cs b/src/Api/Billing/Models/Responses/ConsolidatedBillingSubscriptionResponse.cs index 71f4b122c..ddfef1a3a 100644 --- a/src/Api/Billing/Models/Responses/ConsolidatedBillingSubscriptionResponse.cs +++ b/src/Api/Billing/Models/Responses/ConsolidatedBillingSubscriptionResponse.cs @@ -26,7 +26,7 @@ public record ConsolidatedBillingSubscriptionResponse( .Select(providerPlan => { 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; return new ProviderPlanResponse( plan.Name, @@ -36,7 +36,9 @@ public record ConsolidatedBillingSubscriptionResponse( cost, cadence); }); + var gracePeriod = subscription.CollectionMethod == "charge_automatically" ? 14 : 30; + return new ConsolidatedBillingSubscriptionResponse( subscription.Status, subscription.CurrentPeriodEnd, diff --git a/src/Api/Models/Response/PlanResponseModel.cs b/src/Api/Models/Response/PlanResponseModel.cs index f9d6959b4..b6ca9b62d 100644 --- a/src/Api/Models/Response/PlanResponseModel.cs +++ b/src/Api/Models/Response/PlanResponseModel.cs @@ -121,8 +121,10 @@ public class PlanResponseModel : ResponseModel { StripePlanId = plan.StripePlanId; StripeSeatPlanId = plan.StripeSeatPlanId; + StripeProviderPortalSeatPlanId = plan.StripeProviderPortalSeatPlanId; BasePrice = plan.BasePrice; SeatPrice = plan.SeatPrice; + ProviderPortalSeatPrice = plan.ProviderPortalSeatPrice; AllowSeatAutoscale = plan.AllowSeatAutoscale; HasAdditionalSeatsOption = plan.HasAdditionalSeatsOption; MaxAdditionalSeats = plan.MaxAdditionalSeats; @@ -141,8 +143,10 @@ public class PlanResponseModel : ResponseModel // Seats public string StripePlanId { get; init; } public string StripeSeatPlanId { get; init; } + public string StripeProviderPortalSeatPlanId { get; init; } public decimal BasePrice { get; init; } public decimal SeatPrice { get; init; } + public decimal ProviderPortalSeatPrice { get; init; } public bool AllowSeatAutoscale { get; init; } public bool HasAdditionalSeatsOption { get; init; } public int? MaxAdditionalSeats { get; init; } diff --git a/src/Billing/Services/Implementations/ProviderEventService.cs b/src/Billing/Services/Implementations/ProviderEventService.cs index 493111b96..6a3ffea07 100644 --- a/src/Billing/Services/Implementations/ProviderEventService.cs +++ b/src/Billing/Services/Implementations/ProviderEventService.cs @@ -67,9 +67,9 @@ public class ProviderEventService( 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 { diff --git a/src/Core/Billing/Models/StaticStore/Plan.cs b/src/Core/Billing/Models/StaticStore/Plan.cs index e6abb34d3..04488c206 100644 --- a/src/Core/Billing/Models/StaticStore/Plan.cs +++ b/src/Core/Billing/Models/StaticStore/Plan.cs @@ -63,8 +63,10 @@ public abstract record Plan // Seats public string StripePlanId { get; init; } public string StripeSeatPlanId { get; init; } + public string StripeProviderPortalSeatPlanId { get; init; } public decimal BasePrice { get; init; } public decimal SeatPrice { get; init; } + public decimal ProviderPortalSeatPrice { get; init; } public bool AllowSeatAutoscale { get; init; } public bool HasAdditionalSeatsOption { get; init; } public int? MaxAdditionalSeats { get; init; } diff --git a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs index f81f84ffc..f1fff0762 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs @@ -92,8 +92,10 @@ public record EnterprisePlan : Plan else { StripeSeatPlanId = "2023-enterprise-seat-monthly"; + StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-monthly-2024"; StripeStoragePlanId = "storage-gb-monthly"; SeatPrice = 7; + ProviderPortalSeatPrice = 6; AdditionalStoragePricePerGb = 0.5M; } } diff --git a/src/Core/Billing/Models/StaticStore/Plans/TeamsPlan.cs b/src/Core/Billing/Models/StaticStore/Plans/TeamsPlan.cs index e0ea93723..7a98c4c30 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/TeamsPlan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/TeamsPlan.cs @@ -86,8 +86,10 @@ public record TeamsPlan : Plan else { StripeSeatPlanId = "2023-teams-org-seat-monthly"; + StripeProviderPortalSeatPlanId = "password-manager-provider-portal-teams-monthly-2024"; StripeStoragePlanId = "storage-gb-monthly"; SeatPrice = 5; + ProviderPortalSeatPrice = 4; AdditionalStoragePricePerGb = 0.5M; } } diff --git a/src/Core/Models/Business/ProviderSubscriptionUpdate.cs b/src/Core/Models/Business/ProviderSubscriptionUpdate.cs index 4ce372bab..cffce8a7a 100644 --- a/src/Core/Models/Business/ProviderSubscriptionUpdate.cs +++ b/src/Core/Models/Business/ProviderSubscriptionUpdate.cs @@ -24,7 +24,9 @@ public class ProviderSubscriptionUpdate : SubscriptionUpdate 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; _newlyPurchasedSeats = newlyPurchasedSeats; } diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index 11d84f7d7..c39b058b6 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -422,7 +422,7 @@ public class ProviderBillingControllerTests Assert.Equal(50, providerTeamsPlan.SeatMinimum); Assert.Equal(10, providerTeamsPlan.PurchasedSeats); 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); var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); @@ -431,7 +431,7 @@ public class ProviderBillingControllerTests Assert.Equal(100, providerEnterprisePlan.SeatMinimum); Assert.Equal(0, providerEnterprisePlan.PurchasedSeats); 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); } #endregion diff --git a/test/Billing.Test/Services/ProviderEventServiceTests.cs b/test/Billing.Test/Services/ProviderEventServiceTests.cs index 82269b601..75bd6f9a2 100644 --- a/test/Billing.Test/Services/ProviderEventServiceTests.cs +++ b/test/Billing.Test/Services/ProviderEventServiceTests.cs @@ -231,7 +231,7 @@ public class ProviderEventServiceTests options.PlanName == "Teams (Monthly)" && options.AssignedSeats == 50 && 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( options => @@ -242,7 +242,7 @@ public class ProviderEventServiceTests options.PlanName == "Enterprise (Monthly)" && options.AssignedSeats == 50 && 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( options => @@ -253,7 +253,7 @@ public class ProviderEventServiceTests options.PlanName == "Teams (Monthly)" && options.AssignedSeats == 50 && 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( options => @@ -264,7 +264,7 @@ public class ProviderEventServiceTests options.PlanName == "Enterprise (Monthly)" && options.AssignedSeats == 50 && options.UsedSeats == 0 && - options.Total == options.AssignedSeats * enterprisePlan.PasswordManager.SeatPrice * 0.65M)); + options.Total == options.AssignedSeats * enterprisePlan.PasswordManager.ProviderPortalSeatPrice * 0.65M)); } [Fact]