using Bit.Billing.Constants; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Stripe; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler { private readonly ILogger _logger; private readonly IStripeEventService _stripeEventService; private readonly IUserService _userService; private readonly IStripeFacade _stripeFacade; private readonly IMailService _mailService; private readonly IProviderRepository _providerRepository; private readonly IValidateSponsorshipCommand _validateSponsorshipCommand; private readonly IOrganizationRepository _organizationRepository; private readonly IStripeEventUtilityService _stripeEventUtilityService; public UpcomingInvoiceHandler( ILogger logger, IStripeEventService stripeEventService, IUserService userService, IStripeFacade stripeFacade, IMailService mailService, IProviderRepository providerRepository, IValidateSponsorshipCommand validateSponsorshipCommand, IOrganizationRepository organizationRepository, IStripeEventUtilityService stripeEventUtilityService) { _logger = logger; _stripeEventService = stripeEventService; _userService = userService; _stripeFacade = stripeFacade; _mailService = mailService; _providerRepository = providerRepository; _validateSponsorshipCommand = validateSponsorshipCommand; _organizationRepository = organizationRepository; _stripeEventUtilityService = stripeEventUtilityService; } /// /// Handles the event type from Stripe. /// /// /// public async Task HandleAsync(Event parsedEvent) { var invoice = await _stripeEventService.GetInvoice(parsedEvent); if (string.IsNullOrEmpty(invoice.SubscriptionId)) { _logger.LogWarning("Received 'invoice.upcoming' Event with ID '{eventId}' that did not include a Subscription ID", parsedEvent.Id); return; } var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId); if (subscription == null) { throw new Exception( $"Received null Subscription from Stripe for ID '{invoice.SubscriptionId}' while processing Event with ID '{parsedEvent.Id}'"); } var updatedSubscription = await TryEnableAutomaticTaxAsync(subscription); var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(updatedSubscription.Metadata); var invoiceLineItemDescriptions = invoice.Lines.Select(i => i.Description).ToList(); if (organizationId.HasValue) { if (_stripeEventUtilityService.IsSponsoredSubscription(updatedSubscription)) { await _validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value); } var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); if (organization == null || !OrgPlanForInvoiceNotifications(organization)) { return; } await SendEmails(new List { organization.BillingEmail }); /* * TODO: https://bitwarden.atlassian.net/browse/PM-4862 * Disabling this as part of a hot fix. It needs to check whether the organization * belongs to a Reseller provider and only send an email to the organization owners if it does. * It also requires a new email template as the current one contains too much billing information. */ // var ownerEmails = await _organizationRepository.GetOwnerEmailAddressesById(organization.Id); // await SendEmails(ownerEmails); } else if (userId.HasValue) { var user = await _userService.GetUserByIdAsync(userId.Value); if (user?.Premium == true) { await SendEmails(new List { user.Email }); } } else if (providerId.HasValue) { var provider = await _providerRepository.GetByIdAsync(providerId.Value); if (provider == null) { _logger.LogError( "Received invoice.Upcoming webhook ({EventID}) for Provider ({ProviderID}) that does not exist", parsedEvent.Id, providerId.Value); return; } await SendEmails(new List { provider.BillingEmail }); } return; /* * Sends emails to the given email addresses. */ async Task SendEmails(IEnumerable emails) { var validEmails = emails.Where(e => !string.IsNullOrEmpty(e)); if (invoice.NextPaymentAttempt.HasValue) { await _mailService.SendInvoiceUpcoming( validEmails, invoice.AmountDue / 100M, invoice.NextPaymentAttempt.Value, invoiceLineItemDescriptions, true); } } } private async Task TryEnableAutomaticTaxAsync(Subscription subscription) { var customerGetOptions = new CustomerGetOptions { Expand = ["tax"] }; var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions); if (subscription.AutomaticTax.Enabled || customer.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.Supported) { return subscription; } var subscriptionUpdateOptions = new SubscriptionUpdateOptions { DefaultTaxRates = [], AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } }; return await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionUpdateOptions); } private static bool OrgPlanForInvoiceNotifications(Organization org) => StaticStore.GetPlan(org.PlanType).IsAnnual; }