1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 13:08:17 -05:00
bitwarden/src/Billing/Services/Implementations/ProviderEventService.cs
Alex Morask 95f54b616e
[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
2024-06-24 11:16:57 -04:00

158 lines
6.9 KiB
C#

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;
using Stripe;
namespace Bit.Billing.Services.Implementations;
public class ProviderEventService(
ILogger<ProviderEventService> logger,
IProviderInvoiceItemRepository providerInvoiceItemRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IProviderPlanRepository providerPlanRepository,
IStripeEventService stripeEventService,
IStripeFacade stripeFacade) : IProviderEventService
{
public async Task TryRecordInvoiceLineItems(Event parsedEvent)
{
if (parsedEvent.Type is not HandledStripeWebhook.InvoiceCreated and not HandledStripeWebhook.InvoiceFinalized)
{
return;
}
var invoice = await stripeEventService.GetInvoice(parsedEvent);
var metadata = (await stripeFacade.GetSubscription(invoice.SubscriptionId)).Metadata ?? new Dictionary<string, string>();
var hasProviderId = metadata.TryGetValue("providerId", out var providerId);
if (!hasProviderId)
{
return;
}
var parsedProviderId = Guid.Parse(providerId);
switch (parsedEvent.Type)
{
case HandledStripeWebhook.InvoiceCreated:
{
var clients =
(await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId))
.Where(providerOrganization => providerOrganization.Status == OrganizationStatusType.Managed);
var providerPlans = await providerPlanRepository.GetByProviderId(parsedProviderId);
var enterpriseProviderPlan =
providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
var teamsProviderPlan =
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);
throw new Exception("Cannot record invoice line items for Provider with missing or misconfigured provider plans");
}
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
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,
ClientName = client.OrganizationName,
PlanName = client.Plan,
AssignedSeats = client.Seats ?? 0,
UsedSeats = client.UserCount,
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;
if (unassignedEnterpriseSeats > 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)
{
var teamsClientSeats = invoiceItems
.Where(item => item.PlanName == teamsPlan.Name)
.Sum(item => item.AssignedSeats);
var unassignedTeamsSeats = teamsProviderPlan.SeatMinimum - teamsClientSeats ?? 0;
if (unassignedTeamsSeats > 0)
{
invoiceItems.Add(new ProviderInvoiceItem
{
ProviderId = parsedProviderId,
InvoiceId = invoice.Id,
InvoiceNumber = invoice.Number,
ClientName = "Unassigned seats",
PlanName = teamsPlan.Name,
AssignedSeats = unassignedTeamsSeats,
UsedSeats = 0,
Total = unassignedTeamsSeats * discountedTeamsSeatPrice
});
}
}
await Task.WhenAll(invoiceItems.Select(providerInvoiceItemRepository.CreateAsync));
break;
}
case HandledStripeWebhook.InvoiceFinalized:
{
var invoiceItems = await providerInvoiceItemRepository.GetByInvoiceId(invoice.Id);
if (invoiceItems.Count != 0)
{
await Task.WhenAll(invoiceItems.Select(invoiceItem =>
{
invoiceItem.InvoiceNumber = invoice.Number;
return providerInvoiceItemRepository.ReplaceAsync(invoiceItem);
}));
}
break;
}
}
}
}