From 25afd50ab4f804a01e05024fd82aebcd2428fdc9 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Tue, 12 Nov 2024 14:53:34 +0100 Subject: [PATCH] [PM-14798] Update ProviderEventService for multi-organization enterprises (#5026) --- .../Implementations/ProviderEventService.cs | 85 +++++++------------ .../Services/ProviderEventServiceTests.cs | 49 ----------- 2 files changed, 29 insertions(+), 105 deletions(-) diff --git a/src/Billing/Services/Implementations/ProviderEventService.cs b/src/Billing/Services/Implementations/ProviderEventService.cs index 7ce72526c..548ed9f54 100644 --- a/src/Billing/Services/Implementations/ProviderEventService.cs +++ b/src/Billing/Services/Implementations/ProviderEventService.cs @@ -1,7 +1,6 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Enums; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.Utilities; @@ -42,78 +41,52 @@ public class ProviderEventService( case HandledStripeWebhook.InvoiceCreated: { var clients = - (await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId)) - .Where(providerOrganization => providerOrganization.Status == OrganizationStatusType.Managed); + await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId); var providerPlans = await providerPlanRepository.GetByProviderId(parsedProviderId); - var enterpriseProviderPlan = - providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly); + var invoiceItems = new List(); - var teamsProviderPlan = - providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly); - - if (enterpriseProviderPlan == null || !enterpriseProviderPlan.IsConfigured() || - teamsProviderPlan == null || !teamsProviderPlan.IsConfigured()) + foreach (var client in clients) { - logger.LogError("Provider {ProviderID} is missing or has misconfigured provider plans", parsedProviderId); + if (client.Status != OrganizationStatusType.Managed) + { + continue; + } - throw new Exception("Cannot record invoice line items for Provider with missing or misconfigured provider plans"); - } + var plan = StaticStore.Plans.Single(x => x.Name == client.Plan && providerPlans.Any(y => y.PlanType == x.Type)); - var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); + var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; - var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); - - var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; - - var discountedEnterpriseSeatPrice = enterprisePlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; - - var discountedTeamsSeatPrice = teamsPlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; - - var invoiceItems = clients.Select(client => new ProviderInvoiceItem - { - ProviderId = parsedProviderId, - InvoiceId = invoice.Id, - InvoiceNumber = invoice.Number, - ClientId = client.OrganizationId, - ClientName = client.OrganizationName, - PlanName = client.Plan, - AssignedSeats = client.Seats ?? 0, - UsedSeats = client.OccupiedSeats ?? 0, - Total = client.Plan == enterprisePlan.Name - ? (client.Seats ?? 0) * discountedEnterpriseSeatPrice - : (client.Seats ?? 0) * discountedTeamsSeatPrice - }).ToList(); - - if (enterpriseProviderPlan.PurchasedSeats is null or 0) - { - var enterpriseClientSeats = invoiceItems - .Where(item => item.PlanName == enterprisePlan.Name) - .Sum(item => item.AssignedSeats); - - var unassignedEnterpriseSeats = enterpriseProviderPlan.SeatMinimum - enterpriseClientSeats ?? 0; + var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; invoiceItems.Add(new ProviderInvoiceItem { ProviderId = parsedProviderId, InvoiceId = invoice.Id, InvoiceNumber = invoice.Number, - ClientName = "Unassigned seats", - PlanName = enterprisePlan.Name, - AssignedSeats = unassignedEnterpriseSeats, - UsedSeats = 0, - Total = unassignedEnterpriseSeats * discountedEnterpriseSeatPrice + ClientId = client.OrganizationId, + ClientName = client.OrganizationName, + PlanName = client.Plan, + AssignedSeats = client.Seats ?? 0, + UsedSeats = client.OccupiedSeats ?? 0, + Total = (client.Seats ?? 0) * discountedSeatPrice }); } - if (teamsProviderPlan.PurchasedSeats is null or 0) + foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0)) { - var teamsClientSeats = invoiceItems - .Where(item => item.PlanName == teamsPlan.Name) + var plan = StaticStore.GetPlan(providerPlan.PlanType); + + var clientSeats = invoiceItems + .Where(item => item.PlanName == plan.Name) .Sum(item => item.AssignedSeats); - var unassignedTeamsSeats = teamsProviderPlan.SeatMinimum - teamsClientSeats ?? 0; + var unassignedSeats = providerPlan.SeatMinimum - clientSeats ?? 0; + + var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; + + var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; invoiceItems.Add(new ProviderInvoiceItem { @@ -121,10 +94,10 @@ public class ProviderEventService( InvoiceId = invoice.Id, InvoiceNumber = invoice.Number, ClientName = "Unassigned seats", - PlanName = teamsPlan.Name, - AssignedSeats = unassignedTeamsSeats, + PlanName = plan.Name, + AssignedSeats = unassignedSeats, UsedSeats = 0, - Total = unassignedTeamsSeats * discountedTeamsSeatPrice + Total = unassignedSeats * discountedSeatPrice }); } diff --git a/test/Billing.Test/Services/ProviderEventServiceTests.cs b/test/Billing.Test/Services/ProviderEventServiceTests.cs index e76cf0d28..31f4ec896 100644 --- a/test/Billing.Test/Services/ProviderEventServiceTests.cs +++ b/test/Billing.Test/Services/ProviderEventServiceTests.cs @@ -8,7 +8,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.Utilities; -using FluentAssertions; using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; @@ -89,54 +88,6 @@ public class ProviderEventServiceTests await _providerOrganizationRepository.DidNotReceiveWithAnyArgs().GetManyDetailsByProviderAsync(Arg.Any()); } - [Fact] - public async Task TryRecordInvoiceLineItems_InvoiceCreated_MisconfiguredProviderPlans_ThrowsException() - { - // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); - - const string subscriptionId = "sub_1"; - var providerId = Guid.NewGuid(); - - var invoice = new Invoice - { - SubscriptionId = subscriptionId - }; - - _stripeEventService.GetInvoice(stripeEvent).Returns(invoice); - - var subscription = new Subscription - { - Metadata = new Dictionary { { "providerId", providerId.ToString() } } - }; - - _stripeFacade.GetSubscription(subscriptionId).Returns(subscription); - - var providerPlans = new List - { - new () - { - Id = Guid.NewGuid(), - ProviderId = providerId, - PlanType = PlanType.TeamsMonthly, - AllocatedSeats = 0, - PurchasedSeats = 0, - SeatMinimum = 100 - } - }; - - _providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans); - - // Act - var function = async () => await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); - - // Assert - await function - .Should() - .ThrowAsync() - .WithMessage("Cannot record invoice line items for Provider with missing or misconfigured provider plans"); - } - [Fact] public async Task TryRecordInvoiceLineItems_InvoiceCreated_Succeeds() {