1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-25 23:02:17 -05:00
bitwarden/src/Billing/Services/Implementations/PaymentSucceededHandler.cs
Addison Beck cd7c4bf6ce
chore: move Installation and Push to platform's domain folders (#5085)
* chore: set up a `CODEOWNERS` space for platform

* chore: move sql objects for `Installation` to platform's domain

* chore: move `Installation` and `PushRelay` code to platform's domain
2025-01-06 18:10:53 +01:00

177 lines
6.9 KiB
C#

using Bit.Billing.Constants;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Context;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations;
public class PaymentSucceededHandler : IPaymentSucceededHandler
{
private readonly ILogger<PaymentSucceededHandler> _logger;
private readonly IStripeEventService _stripeEventService;
private readonly IOrganizationService _organizationService;
private readonly IUserService _userService;
private readonly IStripeFacade _stripeFacade;
private readonly IProviderRepository _providerRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
private readonly IUserRepository _userRepository;
private readonly IStripeEventUtilityService _stripeEventUtilityService;
private readonly IPushNotificationService _pushNotificationService;
public PaymentSucceededHandler(
ILogger<PaymentSucceededHandler> logger,
IStripeEventService stripeEventService,
IStripeFacade stripeFacade,
IProviderRepository providerRepository,
IOrganizationRepository organizationRepository,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IUserRepository userRepository,
IStripeEventUtilityService stripeEventUtilityService,
IUserService userService,
IOrganizationService organizationService,
IPushNotificationService pushNotificationService)
{
_logger = logger;
_stripeEventService = stripeEventService;
_stripeFacade = stripeFacade;
_providerRepository = providerRepository;
_organizationRepository = organizationRepository;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_userRepository = userRepository;
_stripeEventUtilityService = stripeEventUtilityService;
_userService = userService;
_organizationService = organizationService;
_pushNotificationService = pushNotificationService;
}
/// <summary>
/// Handles the <see cref="HandledStripeWebhook.PaymentSucceeded"/> event type from Stripe.
/// </summary>
/// <param name="parsedEvent"></param>
public async Task HandleAsync(Event parsedEvent)
{
var invoice = await _stripeEventService.GetInvoice(parsedEvent, true);
if (!invoice.Paid || invoice.BillingReason != "subscription_create")
{
return;
}
var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId);
if (subscription?.Status != StripeSubscriptionStatus.Active)
{
return;
}
if (DateTime.UtcNow - invoice.Created < TimeSpan.FromMinutes(1))
{
await Task.Delay(5000);
}
var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
if (providerId.HasValue)
{
var provider = await _providerRepository.GetByIdAsync(providerId.Value);
if (provider == null)
{
_logger.LogError(
"Received invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) that does not exist",
parsedEvent.Id,
providerId.Value);
return;
}
var teamsMonthly = StaticStore.GetPlan(PlanType.TeamsMonthly);
var enterpriseMonthly = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
var teamsMonthlyLineItem =
subscription.Items.Data.FirstOrDefault(item =>
item.Plan.Id == teamsMonthly.PasswordManager.StripeSeatPlanId);
var enterpriseMonthlyLineItem =
subscription.Items.Data.FirstOrDefault(item =>
item.Plan.Id == enterpriseMonthly.PasswordManager.StripeSeatPlanId);
if (teamsMonthlyLineItem == null || enterpriseMonthlyLineItem == null)
{
_logger.LogError("invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items",
parsedEvent.Id,
provider.Id);
return;
}
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
{
Type = ReferenceEventType.Rebilled,
Source = ReferenceEventSource.Provider,
Id = provider.Id,
PlanType = PlanType.TeamsMonthly,
Seats = (int)teamsMonthlyLineItem.Quantity
});
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
{
Type = ReferenceEventType.Rebilled,
Source = ReferenceEventSource.Provider,
Id = provider.Id,
PlanType = PlanType.EnterpriseMonthly,
Seats = (int)enterpriseMonthlyLineItem.Quantity
});
}
else if (organizationId.HasValue)
{
if (!subscription.Items.Any(i =>
StaticStore.Plans.Any(p => p.PasswordManager.StripePlanId == i.Plan.Id)))
{
return;
}
await _organizationService.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Rebilled, organization, _currentContext)
{
PlanName = organization?.Plan,
PlanType = organization?.PlanType,
Seats = organization?.Seats,
Storage = organization?.MaxStorageGb,
});
}
else if (userId.HasValue)
{
if (subscription.Items.All(i => i.Plan.Id != IStripeEventUtilityService.PremiumPlanId))
{
return;
}
await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd);
var user = await _userRepository.GetByIdAsync(userId.Value);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Rebilled, user, _currentContext)
{
PlanName = IStripeEventUtilityService.PremiumPlanId,
Storage = user?.MaxStorageGb,
});
}
}
}