From 5709ea36f4d174049704ac83669158fa5a073b68 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:03:09 -0500 Subject: [PATCH] [PM-15485] Add provider plan details to provider Admin pages (#5326) * Add Provider Plan details to Provider Admin pages * Run dotnet format * Thomas' feedback * Updated code ownership * Robert's feedback * Thomas' feedback --- src/Admin/Admin.csproj | 1 - .../Controllers/ProvidersController.cs | 3 +- .../AdminConsole/Models/ProviderEditModel.cs | 2 +- .../AdminConsole/Models/ProviderViewModel.cs | 49 +++++++++++++++++-- .../AdminConsole/Views/Providers/Edit.cshtml | 4 ++ .../AdminConsole/Views/Providers/View.cshtml | 4 ++ .../Billing/Models/ProviderPlanViewModel.cs | 26 ++++++++++ .../Views/Providers/ProviderPlans.cshtml | 18 +++++++ ...ProviderOrganizationOrganizationDetails.cs | 2 + ...rganizationDetailsReadByProviderIdQuery.cs | 1 + ...derOrganizationOrganizationDetailsView.sql | 1 + ...derOrganizationOrganizationDetailsView.sql | 23 +++++++++ 12 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 src/Admin/Billing/Models/ProviderPlanViewModel.cs create mode 100644 src/Admin/Billing/Views/Providers/ProviderPlans.cshtml create mode 100644 util/Migrator/DbScripts/2025-01-29_00_AddPlanTypeToProviderOrganizationOrganizationDetailsView.sql diff --git a/src/Admin/Admin.csproj b/src/Admin/Admin.csproj index 5493e65afd..4a255eefb2 100644 --- a/src/Admin/Admin.csproj +++ b/src/Admin/Admin.csproj @@ -16,7 +16,6 @@ - diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 8a56483a60..6229a4deab 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -235,7 +235,8 @@ public class ProvidersController : Controller var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id); var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id); - return View(new ProviderViewModel(provider, users, providerOrganizations)); + var providerPlans = await _providerPlanRepository.GetByProviderId(id); + return View(new ProviderViewModel(provider, users, providerOrganizations, providerPlans.ToList())); } [SelfHosted(NotSelfHostedOnly = true)] diff --git a/src/Admin/AdminConsole/Models/ProviderEditModel.cs b/src/Admin/AdminConsole/Models/ProviderEditModel.cs index 7fd5c765c8..bcdf602c07 100644 --- a/src/Admin/AdminConsole/Models/ProviderEditModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderEditModel.cs @@ -19,7 +19,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject IEnumerable organizations, IReadOnlyCollection providerPlans, string gatewayCustomerUrl = null, - string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations) + string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations, providerPlans) { Name = provider.DisplayName(); BusinessName = provider.DisplayBusinessName(); diff --git a/src/Admin/AdminConsole/Models/ProviderViewModel.cs b/src/Admin/AdminConsole/Models/ProviderViewModel.cs index 9c4d07e8bf..724e6220b3 100644 --- a/src/Admin/AdminConsole/Models/ProviderViewModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderViewModel.cs @@ -1,6 +1,9 @@ -using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Admin.Billing.Models; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Enums; namespace Bit.Admin.AdminConsole.Models; @@ -8,17 +11,57 @@ public class ProviderViewModel { public ProviderViewModel() { } - public ProviderViewModel(Provider provider, IEnumerable providerUsers, IEnumerable organizations) + public ProviderViewModel( + Provider provider, + IEnumerable providerUsers, + IEnumerable organizations, + IReadOnlyCollection providerPlans) { Provider = provider; UserCount = providerUsers.Count(); ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin); - ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id); + + if (Provider.Type == ProviderType.Msp) + { + var usedTeamsSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.TeamsMonthly) + .Sum(po => po.OccupiedSeats) ?? 0; + var teamsProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.TeamsMonthly); + if (teamsProviderPlan != null && teamsProviderPlan.IsConfigured()) + { + ProviderPlanViewModels.Add(new ProviderPlanViewModel("Teams (Monthly) Subscription", teamsProviderPlan, usedTeamsSeats)); + } + + var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly) + .Sum(po => po.OccupiedSeats) ?? 0; + var enterpriseProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.EnterpriseMonthly); + if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured()) + { + ProviderPlanViewModels.Add(new ProviderPlanViewModel("Enterprise (Monthly) Subscription", enterpriseProviderPlan, usedEnterpriseSeats)); + } + } + else if (Provider.Type == ProviderType.MultiOrganizationEnterprise) + { + var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly) + .Sum(po => po.OccupiedSeats).GetValueOrDefault(0); + var enterpriseProviderPlan = providerPlans.FirstOrDefault(); + if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured()) + { + var planLabel = enterpriseProviderPlan.PlanType switch + { + PlanType.EnterpriseMonthly => "Enterprise (Monthly) Subscription", + PlanType.EnterpriseAnnually => "Enterprise (Annually) Subscription", + _ => string.Empty + }; + + ProviderPlanViewModels.Add(new ProviderPlanViewModel(planLabel, enterpriseProviderPlan, usedEnterpriseSeats)); + } + } } public int UserCount { get; set; } public Provider Provider { get; set; } public IEnumerable ProviderAdmins { get; set; } public IEnumerable ProviderOrganizations { get; set; } + public List ProviderPlanViewModels { get; set; } = []; } diff --git a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml index 43d72338be..be13a7c740 100644 --- a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml @@ -17,6 +17,10 @@

Provider Information

@await Html.PartialAsync("_ViewInformation", Model) +@if (Model.ProviderPlanViewModels.Any()) +{ + @await Html.PartialAsync("~/Billing/Views/Providers/ProviderPlans.cshtml", Model.ProviderPlanViewModels) +} @await Html.PartialAsync("Admins", Model)
diff --git a/src/Admin/AdminConsole/Views/Providers/View.cshtml b/src/Admin/AdminConsole/Views/Providers/View.cshtml index 0ae31627fc..0774ee2f70 100644 --- a/src/Admin/AdminConsole/Views/Providers/View.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/View.cshtml @@ -7,5 +7,9 @@

Information

@await Html.PartialAsync("_ViewInformation", Model) +@if (Model.ProviderPlanViewModels.Any()) +{ + @await Html.PartialAsync("ProviderPlans", Model.ProviderPlanViewModels) +} @await Html.PartialAsync("Admins", Model) @await Html.PartialAsync("Organizations", Model) diff --git a/src/Admin/Billing/Models/ProviderPlanViewModel.cs b/src/Admin/Billing/Models/ProviderPlanViewModel.cs new file mode 100644 index 0000000000..7a50aba286 --- /dev/null +++ b/src/Admin/Billing/Models/ProviderPlanViewModel.cs @@ -0,0 +1,26 @@ +using Bit.Core.Billing.Entities; + +namespace Bit.Admin.Billing.Models; + +public class ProviderPlanViewModel +{ + public string Name { get; set; } + public int PurchasedSeats { get; set; } + public int AssignedSeats { get; set; } + public int UsedSeats { get; set; } + public int RemainingSeats { get; set; } + + public ProviderPlanViewModel( + string name, + ProviderPlan providerPlan, + int usedSeats) + { + var purchasedSeats = (providerPlan.SeatMinimum ?? 0) + (providerPlan.PurchasedSeats ?? 0); + + Name = name; + PurchasedSeats = purchasedSeats; + AssignedSeats = providerPlan.AllocatedSeats ?? 0; + UsedSeats = usedSeats; + RemainingSeats = purchasedSeats - AssignedSeats; + } +} diff --git a/src/Admin/Billing/Views/Providers/ProviderPlans.cshtml b/src/Admin/Billing/Views/Providers/ProviderPlans.cshtml new file mode 100644 index 0000000000..e84f5a2779 --- /dev/null +++ b/src/Admin/Billing/Views/Providers/ProviderPlans.cshtml @@ -0,0 +1,18 @@ +@model List +@foreach (var plan in Model) +{ +

@plan.Name

+
+
Purchased Seats
+
@plan.PurchasedSeats
+ +
Assigned Seats
+
@plan.AssignedSeats
+ +
Used Seats
+
@plan.UsedSeats
+ +
Remaining Seats
+
@plan.RemainingSeats
+
+} diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs index 1b2112707c..9d84f60c4c 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.Json.Serialization; +using Bit.Core.Billing.Enums; using Bit.Core.Enums; using Bit.Core.Utilities; @@ -23,6 +24,7 @@ public class ProviderOrganizationOrganizationDetails public int? OccupiedSeats { get; set; } public int? Seats { get; set; } public string Plan { get; set; } + public PlanType PlanType { get; set; } public OrganizationStatusType Status { get; set; } /// diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationOrganizationDetailsReadByProviderIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationOrganizationDetailsReadByProviderIdQuery.cs index 62e46566d7..4f99391a24 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationOrganizationDetailsReadByProviderIdQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationOrganizationDetailsReadByProviderIdQuery.cs @@ -35,6 +35,7 @@ public class ProviderOrganizationOrganizationDetailsReadByProviderIdQuery : IQue OccupiedSeats = x.o.OrganizationUsers.Count(ou => ou.Status >= 0), Seats = x.o.Seats, Plan = x.o.Plan, + PlanType = x.o.PlanType, Status = x.o.Status }); } diff --git a/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql b/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql index 0fcff73699..3a08418ed3 100644 --- a/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql @@ -13,6 +13,7 @@ SELECT (SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status >= 0) OccupiedSeats, O.[Seats], O.[Plan], + O.[PlanType], O.[Status] FROM [dbo].[ProviderOrganization] PO diff --git a/util/Migrator/DbScripts/2025-01-29_00_AddPlanTypeToProviderOrganizationOrganizationDetailsView.sql b/util/Migrator/DbScripts/2025-01-29_00_AddPlanTypeToProviderOrganizationOrganizationDetailsView.sql new file mode 100644 index 0000000000..df4c145b71 --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-29_00_AddPlanTypeToProviderOrganizationOrganizationDetailsView.sql @@ -0,0 +1,23 @@ +-- Add column 'PlanType' +CREATE OR AlTER VIEW [dbo].[ProviderOrganizationOrganizationDetailsView] +AS +SELECT + PO.[Id], + PO.[ProviderId], + PO.[OrganizationId], + O.[Name] OrganizationName, + PO.[Key], + PO.[Settings], + PO.[CreationDate], + PO.[RevisionDate], + (SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status = 2) UserCount, + (SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status >= 0) OccupiedSeats, + O.[Seats], + O.[Plan], + O.[PlanType], + O.[Status] +FROM + [dbo].[ProviderOrganization] PO + LEFT JOIN + [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId] +GO