1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[PM-14798] Update ProviderEventService for multi-organization enterprises (#5026)

This commit is contained in:
Jonas Hendrickx 2024-11-12 14:53:34 +01:00 committed by GitHub
parent 702a81b161
commit 25afd50ab4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 29 additions and 105 deletions

View File

@ -1,7 +1,6 @@
using Bit.Billing.Constants; using Bit.Billing.Constants;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -42,36 +41,26 @@ public class ProviderEventService(
case HandledStripeWebhook.InvoiceCreated: case HandledStripeWebhook.InvoiceCreated:
{ {
var clients = var clients =
(await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId)) await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId);
.Where(providerOrganization => providerOrganization.Status == OrganizationStatusType.Managed);
var providerPlans = await providerPlanRepository.GetByProviderId(parsedProviderId); var providerPlans = await providerPlanRepository.GetByProviderId(parsedProviderId);
var enterpriseProviderPlan = var invoiceItems = new List<ProviderInvoiceItem>();
providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
var teamsProviderPlan = foreach (var client in clients)
providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
if (enterpriseProviderPlan == null || !enterpriseProviderPlan.IsConfigured() ||
teamsProviderPlan == null || !teamsProviderPlan.IsConfigured())
{ {
logger.LogError("Provider {ProviderID} is missing or has misconfigured provider plans", parsedProviderId); if (client.Status != OrganizationStatusType.Managed)
{
throw new Exception("Cannot record invoice line items for Provider with missing or misconfigured provider plans"); continue;
} }
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly); var plan = StaticStore.Plans.Single(x => x.Name == client.Plan && providerPlans.Any(y => y.PlanType == x.Type));
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
var discountedEnterpriseSeatPrice = enterprisePlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;
var discountedTeamsSeatPrice = teamsPlan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; invoiceItems.Add(new ProviderInvoiceItem
var invoiceItems = clients.Select(client => new ProviderInvoiceItem
{ {
ProviderId = parsedProviderId, ProviderId = parsedProviderId,
InvoiceId = invoice.Id, InvoiceId = invoice.Id,
@ -81,39 +70,23 @@ public class ProviderEventService(
PlanName = client.Plan, PlanName = client.Plan,
AssignedSeats = client.Seats ?? 0, AssignedSeats = client.Seats ?? 0,
UsedSeats = client.OccupiedSeats ?? 0, UsedSeats = client.OccupiedSeats ?? 0,
Total = client.Plan == enterprisePlan.Name Total = (client.Seats ?? 0) * discountedSeatPrice
? (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;
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
}); });
} }
if (teamsProviderPlan.PurchasedSeats is null or 0) foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0))
{ {
var teamsClientSeats = invoiceItems var plan = StaticStore.GetPlan(providerPlan.PlanType);
.Where(item => item.PlanName == teamsPlan.Name)
var clientSeats = invoiceItems
.Where(item => item.PlanName == plan.Name)
.Sum(item => item.AssignedSeats); .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 invoiceItems.Add(new ProviderInvoiceItem
{ {
@ -121,10 +94,10 @@ public class ProviderEventService(
InvoiceId = invoice.Id, InvoiceId = invoice.Id,
InvoiceNumber = invoice.Number, InvoiceNumber = invoice.Number,
ClientName = "Unassigned seats", ClientName = "Unassigned seats",
PlanName = teamsPlan.Name, PlanName = plan.Name,
AssignedSeats = unassignedTeamsSeats, AssignedSeats = unassignedSeats,
UsedSeats = 0, UsedSeats = 0,
Total = unassignedTeamsSeats * discountedTeamsSeatPrice Total = unassignedSeats * discountedSeatPrice
}); });
} }

View File

@ -8,7 +8,6 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using FluentAssertions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using Stripe; using Stripe;
@ -89,54 +88,6 @@ public class ProviderEventServiceTests
await _providerOrganizationRepository.DidNotReceiveWithAnyArgs().GetManyDetailsByProviderAsync(Arg.Any<Guid>()); await _providerOrganizationRepository.DidNotReceiveWithAnyArgs().GetManyDetailsByProviderAsync(Arg.Any<Guid>());
} }
[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<string, string> { { "providerId", providerId.ToString() } }
};
_stripeFacade.GetSubscription(subscriptionId).Returns(subscription);
var providerPlans = new List<ProviderPlan>
{
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<Exception>()
.WithMessage("Cannot record invoice line items for Provider with missing or misconfigured provider plans");
}
[Fact] [Fact]
public async Task TryRecordInvoiceLineItems_InvoiceCreated_Succeeds() public async Task TryRecordInvoiceLineItems_InvoiceCreated_Succeeds()
{ {