1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

[AC-1693] Send InvoiceUpcoming Notification to Client Owners (#3319)

* Add Organization_ReadOwnerEmailAddresses SPROC

* Add IOrganizationRepository.GetOwnerEmailAddressesById

* Add SendInvoiceUpcoming overload for multiple emails

* Update InvoiceUpcoming handler to send multiple emails

* Cy's feedback

* Updates from testing

Hardened against missing entity IDs in Stripe events in the StripeEventService. Updated ValidateCloudRegion to not use a refresh/expansion for the customer because the invoice.upcoming event does not have an invoice.Id. Updated the StripeController's handling of invoice.upcoming to not use a refresh/expansion for the subscription because the invoice does not have an ID.

* Fix broken test
This commit is contained in:
Alex Morask
2023-10-23 13:46:29 -04:00
committed by GitHub
parent 18b43130e8
commit c442bae2bc
10 changed files with 190 additions and 39 deletions

View File

@ -52,6 +52,7 @@ public class StripeController : Controller
private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly IStripeEventService _stripeEventService;
private readonly IStripeFacade _stripeFacade;
public StripeController(
GlobalSettings globalSettings,
@ -70,7 +71,8 @@ public class StripeController : Controller
ITaxRateRepository taxRateRepository,
IUserRepository userRepository,
ICurrentContext currentContext,
IStripeEventService stripeEventService)
IStripeEventService stripeEventService,
IStripeFacade stripeFacade)
{
_billingSettings = billingSettings?.Value;
_hostingEnvironment = hostingEnvironment;
@ -97,6 +99,7 @@ public class StripeController : Controller
_currentContext = currentContext;
_globalSettings = globalSettings;
_stripeEventService = stripeEventService;
_stripeFacade = stripeFacade;
}
[HttpPost("webhook")]
@ -209,48 +212,71 @@ public class StripeController : Controller
else if (parsedEvent.Type.Equals(HandledStripeWebhook.UpcomingInvoice))
{
var invoice = await _stripeEventService.GetInvoice(parsedEvent);
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
if (string.IsNullOrEmpty(invoice.SubscriptionId))
{
_logger.LogWarning("Received 'invoice.upcoming' Event with ID '{eventId}' that did not include a Subscription ID", parsedEvent.Id);
return new OkResult();
}
var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId);
if (subscription == null)
{
throw new Exception("Invoice subscription is null. " + invoice.Id);
throw new Exception(
$"Received null Subscription from Stripe for ID '{invoice.SubscriptionId}' while processing Event with ID '{parsedEvent.Id}'");
}
subscription = await VerifyCorrectTaxRateForCharge(invoice, subscription);
var updatedSubscription = await VerifyCorrectTaxRateForCharge(invoice, subscription);
string email = null;
var ids = GetIdsFromMetaData(subscription.Metadata);
// org
if (ids.Item1.HasValue)
var (organizationId, userId) = GetIdsFromMetaData(updatedSubscription.Metadata);
var invoiceLineItemDescriptions = invoice.Lines.Select(i => i.Description).ToList();
async Task SendEmails(IEnumerable<string> emails)
{
// sponsored org
if (IsSponsoredSubscription(subscription))
{
await _validateSponsorshipCommand.ValidateSponsorshipAsync(ids.Item1.Value);
}
var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));
var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value);
if (org != null && OrgPlanForInvoiceNotifications(org))
if (invoice.NextPaymentAttempt.HasValue)
{
email = org.BillingEmail;
await _mailService.SendInvoiceUpcoming(
validEmails,
invoice.AmountDue / 100M,
invoice.NextPaymentAttempt.Value,
invoiceLineItemDescriptions,
true);
}
}
// user
else if (ids.Item2.HasValue)
if (organizationId.HasValue)
{
var user = await _userService.GetUserByIdAsync(ids.Item2.Value);
if (IsSponsoredSubscription(updatedSubscription))
{
await _validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value);
}
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
if (organization == null || !OrgPlanForInvoiceNotifications(organization))
{
return new OkResult();
}
await SendEmails(new List<string> { organization.BillingEmail });
var ownerEmails = await _organizationRepository.GetOwnerEmailAddressesById(organization.Id);
await SendEmails(ownerEmails);
}
else if (userId.HasValue)
{
var user = await _userService.GetUserByIdAsync(userId.Value);
if (user.Premium)
{
email = user.Email;
await SendEmails(new List<string> { user.Email });
}
}
if (!string.IsNullOrWhiteSpace(email) && invoice.NextPaymentAttempt.HasValue)
{
var items = invoice.Lines.Select(i => i.Description).ToList();
await _mailService.SendInvoiceUpcomingAsync(email, invoice.AmountDue / 100M,
invoice.NextPaymentAttempt.Value, items, true);
}
}
else if (parsedEvent.Type.Equals(HandledStripeWebhook.ChargeSucceeded))
{