mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 13:08:17 -05:00

* 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
158 lines
6.9 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|