Skip to content

Commit 27c099d

Browse files
Handle customer.updated event in StripeController
1 parent 16847f7 commit 27c099d

File tree

3 files changed

+39
-207
lines changed

3 files changed

+39
-207
lines changed

src/Billing/Constants/HandledStripeWebhook.cs

+1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ public static class HandledStripeWebhook
1111
public const string PaymentFailed = "invoice.payment_failed";
1212
public const string InvoiceCreated = "invoice.created";
1313
public const string PaymentMethodAttached = "payment_method.attached";
14+
public const string CustomerUpdated = "customer.updated";
1415
}

src/Billing/Controllers/StripeController.cs

+36-207
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Bit.Billing.Constants;
2+
using Bit.Billing.Services;
23
using Bit.Core.Context;
34
using Bit.Core.Entities;
45
using Bit.Core.Enums;
@@ -44,12 +45,13 @@ public class StripeController : Controller
4445
private readonly IAppleIapService _appleIapService;
4546
private readonly IMailService _mailService;
4647
private readonly ILogger<StripeController> _logger;
47-
private readonly Braintree.BraintreeGateway _btGateway;
48+
private readonly BraintreeGateway _btGateway;
4849
private readonly IReferenceEventService _referenceEventService;
4950
private readonly ITaxRateRepository _taxRateRepository;
5051
private readonly IUserRepository _userRepository;
5152
private readonly ICurrentContext _currentContext;
5253
private readonly GlobalSettings _globalSettings;
54+
private readonly IStripeEventService _stripeEventService;
5355

5456
public StripeController(
5557
GlobalSettings globalSettings,
@@ -67,7 +69,8 @@ public StripeController(
6769
ILogger<StripeController> logger,
6870
ITaxRateRepository taxRateRepository,
6971
IUserRepository userRepository,
70-
ICurrentContext currentContext)
72+
ICurrentContext currentContext,
73+
IStripeEventService stripeEventService)
7174
{
7275
_billingSettings = billingSettings?.Value;
7376
_hostingEnvironment = hostingEnvironment;
@@ -83,7 +86,7 @@ public StripeController(
8386
_taxRateRepository = taxRateRepository;
8487
_userRepository = userRepository;
8588
_logger = logger;
86-
_btGateway = new Braintree.BraintreeGateway
89+
_btGateway = new BraintreeGateway
8790
{
8891
Environment = globalSettings.Braintree.Production ?
8992
Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,
@@ -93,6 +96,7 @@ public StripeController(
9396
};
9497
_currentContext = currentContext;
9598
_globalSettings = globalSettings;
99+
_stripeEventService = stripeEventService;
96100
}
97101

98102
[HttpPost("webhook")]
@@ -103,7 +107,7 @@ public async Task<IActionResult> PostWebhook([FromQuery] string key)
103107
return new BadRequestResult();
104108
}
105109

106-
Stripe.Event parsedEvent;
110+
Event parsedEvent;
107111
using (var sr = new StreamReader(HttpContext.Request.Body))
108112
{
109113
var json = await sr.ReadToEndAsync();
@@ -125,7 +129,7 @@ public async Task<IActionResult> PostWebhook([FromQuery] string key)
125129
}
126130

127131
// If the customer and server cloud regions don't match, early return 200 to avoid unnecessary errors
128-
if (!await ValidateCloudRegionAsync(parsedEvent))
132+
if (!await _stripeEventService.ValidateCloudRegion(parsedEvent))
129133
{
130134
return new OkResult();
131135
}
@@ -135,7 +139,7 @@ public async Task<IActionResult> PostWebhook([FromQuery] string key)
135139

136140
if (subDeleted || subUpdated)
137141
{
138-
var subscription = await GetSubscriptionAsync(parsedEvent, true);
142+
var subscription = await _stripeEventService.GetSubscription(parsedEvent, true);
139143
var ids = GetIdsFromMetaData(subscription.Metadata);
140144
var organizationId = ids.Item1 ?? Guid.Empty;
141145
var userId = ids.Item2 ?? Guid.Empty;
@@ -204,7 +208,7 @@ await _userService.UpdatePremiumExpirationAsync(userId,
204208
}
205209
else if (parsedEvent.Type.Equals(HandledStripeWebhook.UpcomingInvoice))
206210
{
207-
var invoice = await GetInvoiceAsync(parsedEvent);
211+
var invoice = await _stripeEventService.GetInvoice(parsedEvent);
208212
var subscriptionService = new SubscriptionService();
209213
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
210214
if (subscription == null)
@@ -250,7 +254,7 @@ await _mailService.SendInvoiceUpcomingAsync(email, invoice.AmountDue / 100M,
250254
}
251255
else if (parsedEvent.Type.Equals(HandledStripeWebhook.ChargeSucceeded))
252256
{
253-
var charge = await GetChargeAsync(parsedEvent);
257+
var charge = await _stripeEventService.GetCharge(parsedEvent);
254258
var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync(
255259
GatewayType.Stripe, charge.Id);
256260
if (chargeTransaction != null)
@@ -377,7 +381,7 @@ await _mailService.SendInvoiceUpcomingAsync(email, invoice.AmountDue / 100M,
377381
}
378382
else if (parsedEvent.Type.Equals(HandledStripeWebhook.ChargeRefunded))
379383
{
380-
var charge = await GetChargeAsync(parsedEvent);
384+
var charge = await _stripeEventService.GetCharge(parsedEvent);
381385
var chargeTransaction = await _transactionRepository.GetByGatewayIdAsync(
382386
GatewayType.Stripe, charge.Id);
383387
if (chargeTransaction == null)
@@ -427,7 +431,7 @@ await _transactionRepository.CreateAsync(new Transaction
427431
}
428432
else if (parsedEvent.Type.Equals(HandledStripeWebhook.PaymentSucceeded))
429433
{
430-
var invoice = await GetInvoiceAsync(parsedEvent, true);
434+
var invoice = await _stripeEventService.GetInvoice(parsedEvent, true);
431435
if (invoice.Paid && invoice.BillingReason == "subscription_create")
432436
{
433437
var subscriptionService = new SubscriptionService();
@@ -479,125 +483,53 @@ await _referenceEventService.RaiseEventAsync(
479483
}
480484
else if (parsedEvent.Type.Equals(HandledStripeWebhook.PaymentFailed))
481485
{
482-
await HandlePaymentFailed(await GetInvoiceAsync(parsedEvent, true));
486+
await HandlePaymentFailed(await _stripeEventService.GetInvoice(parsedEvent, true));
483487
}
484488
else if (parsedEvent.Type.Equals(HandledStripeWebhook.InvoiceCreated))
485489
{
486-
var invoice = await GetInvoiceAsync(parsedEvent, true);
490+
var invoice = await _stripeEventService.GetInvoice(parsedEvent, true);
487491
if (!invoice.Paid && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice))
488492
{
489493
await AttemptToPayInvoiceAsync(invoice);
490494
}
491495
}
492496
else if (parsedEvent.Type.Equals(HandledStripeWebhook.PaymentMethodAttached))
493497
{
494-
var paymentMethod = await GetPaymentMethodAsync(parsedEvent);
498+
var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent);
495499
await HandlePaymentMethodAttachedAsync(paymentMethod);
496500
}
497-
else
498-
{
499-
_logger.LogWarning("Unsupported event received. " + parsedEvent.Type);
500-
}
501-
502-
return new OkResult();
503-
}
504-
505-
/// <summary>
506-
/// Ensures that the customer associated with the parsed event's data is in the correct region for this server.
507-
/// We use the customer instead of the subscription given that all subscriptions have customers, but not all
508-
/// customers have subscriptions
509-
/// </summary>
510-
/// <param name="parsedEvent"></param>
511-
/// <returns>true if the customer's region and the server's region match, otherwise false</returns>
512-
/// <exception cref="Exception"></exception>
513-
private async Task<bool> ValidateCloudRegionAsync(Event parsedEvent)
514-
{
515-
var serverRegion = _globalSettings.BaseServiceUri.CloudRegion;
516-
var eventType = parsedEvent.Type;
517-
var expandOptions = new List<string> { "customer" };
518-
519-
try
501+
else if (parsedEvent.Type.Equals(HandledStripeWebhook.CustomerUpdated))
520502
{
521-
Dictionary<string, string> customerMetadata;
522-
switch (eventType)
523-
{
524-
case HandledStripeWebhook.SubscriptionDeleted:
525-
case HandledStripeWebhook.SubscriptionUpdated:
526-
customerMetadata = (await GetSubscriptionAsync(parsedEvent, true, expandOptions))?.Customer
527-
?.Metadata;
528-
break;
529-
case HandledStripeWebhook.ChargeSucceeded:
530-
case HandledStripeWebhook.ChargeRefunded:
531-
customerMetadata = (await GetChargeAsync(parsedEvent, true, expandOptions))?.Customer?.Metadata;
532-
break;
533-
case HandledStripeWebhook.UpcomingInvoice:
534-
customerMetadata = (await GetInvoiceAsync(parsedEvent))?.Customer?.Metadata;
535-
break;
536-
case HandledStripeWebhook.PaymentSucceeded:
537-
case HandledStripeWebhook.PaymentFailed:
538-
case HandledStripeWebhook.InvoiceCreated:
539-
customerMetadata = (await GetInvoiceAsync(parsedEvent, true, expandOptions))?.Customer?.Metadata;
540-
break;
541-
case HandledStripeWebhook.PaymentMethodAttached:
542-
customerMetadata = (await GetPaymentMethodAsync(parsedEvent, true, expandOptions))
543-
?.Customer
544-
?.Metadata;
545-
break;
546-
default:
547-
customerMetadata = null;
548-
break;
549-
}
503+
var customer =
504+
await _stripeEventService.GetCustomer(parsedEvent, true, new List<string> { "subscriptions" });
550505

551-
if (customerMetadata is null)
506+
if (customer.Subscriptions == null || !customer.Subscriptions.Any())
552507
{
553-
return false;
508+
return new OkResult();
554509
}
555510

556-
var customerRegion = GetCustomerRegionFromMetadata(customerMetadata);
511+
var subscription = customer.Subscriptions.First();
557512

558-
return customerRegion == serverRegion;
559-
}
560-
catch (Exception e)
561-
{
562-
_logger.LogError(e, "Encountered unexpected error while validating cloud region");
563-
throw;
564-
}
565-
}
513+
var (organizationId, _) = GetIdsFromMetaData(subscription.Metadata);
566514

567-
/// <summary>
568-
/// Gets the customer's region from the metadata.
569-
/// </summary>
570-
/// <param name="customerMetadata">The metadata of the customer.</param>
571-
/// <returns>The region of the customer. If the region is not specified, it returns "US", if metadata is null,
572-
/// it returns null. It is case insensitive.</returns>
573-
private static string GetCustomerRegionFromMetadata(IDictionary<string, string> customerMetadata)
574-
{
575-
const string defaultRegion = "US";
515+
if (!organizationId.HasValue)
516+
{
517+
return new OkResult();
518+
}
576519

577-
if (customerMetadata is null)
578-
{
579-
return null;
580-
}
520+
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
521+
organization.BillingEmail = customer.Email;
522+
await _organizationRepository.ReplaceAsync(organization);
581523

582-
if (customerMetadata.TryGetValue("region", out var value))
583-
{
584-
return value;
524+
await _referenceEventService.RaiseEventAsync(
525+
new ReferenceEvent(ReferenceEventType.OrganizationEditedInStripe, organization, _currentContext));
585526
}
586-
587-
var miscasedRegionKey = customerMetadata.Keys
588-
.FirstOrDefault(key =>
589-
key.Equals("region", StringComparison.OrdinalIgnoreCase));
590-
591-
if (miscasedRegionKey is null)
527+
else
592528
{
593-
return defaultRegion;
529+
_logger.LogWarning("Unsupported event received. " + parsedEvent.Type);
594530
}
595531

596-
_ = customerMetadata.TryGetValue(miscasedRegionKey, out var regionValue);
597-
598-
return !string.IsNullOrWhiteSpace(regionValue)
599-
? regionValue
600-
: defaultRegion;
532+
return new OkResult();
601533
}
602534

603535
private async Task HandlePaymentMethodAttachedAsync(PaymentMethod paymentMethod)
@@ -975,109 +907,6 @@ private bool UnpaidAutoChargeInvoiceForSubscriptionCycle(Invoice invoice)
975907
invoice.BillingReason == "subscription_cycle" && invoice.SubscriptionId != null;
976908
}
977909

978-
private async Task<Charge> GetChargeAsync(Event parsedEvent, bool fresh = false, List<string> expandOptions = null)
979-
{
980-
if (!(parsedEvent.Data.Object is Charge eventCharge))
981-
{
982-
throw new Exception("Charge is null (from parsed event). " + parsedEvent.Id);
983-
}
984-
if (!fresh)
985-
{
986-
return eventCharge;
987-
}
988-
var chargeService = new ChargeService();
989-
var chargeGetOptions = new ChargeGetOptions { Expand = expandOptions };
990-
var charge = await chargeService.GetAsync(eventCharge.Id, chargeGetOptions);
991-
if (charge == null)
992-
{
993-
throw new Exception("Charge is null. " + eventCharge.Id);
994-
}
995-
return charge;
996-
}
997-
998-
private async Task<Invoice> GetInvoiceAsync(Stripe.Event parsedEvent, bool fresh = false, List<string> expandOptions = null)
999-
{
1000-
if (!(parsedEvent.Data.Object is Invoice eventInvoice))
1001-
{
1002-
throw new Exception("Invoice is null (from parsed event). " + parsedEvent.Id);
1003-
}
1004-
if (!fresh)
1005-
{
1006-
return eventInvoice;
1007-
}
1008-
var invoiceService = new InvoiceService();
1009-
var invoiceGetOptions = new InvoiceGetOptions { Expand = expandOptions };
1010-
var invoice = await invoiceService.GetAsync(eventInvoice.Id, invoiceGetOptions);
1011-
if (invoice == null)
1012-
{
1013-
throw new Exception("Invoice is null. " + eventInvoice.Id);
1014-
}
1015-
return invoice;
1016-
}
1017-
1018-
private async Task<Subscription> GetSubscriptionAsync(Stripe.Event parsedEvent, bool fresh = false,
1019-
List<string> expandOptions = null)
1020-
{
1021-
if (parsedEvent.Data.Object is not Subscription eventSubscription)
1022-
{
1023-
throw new Exception("Subscription is null (from parsed event). " + parsedEvent.Id);
1024-
}
1025-
if (!fresh)
1026-
{
1027-
return eventSubscription;
1028-
}
1029-
var subscriptionService = new SubscriptionService();
1030-
var subscriptionGetOptions = new SubscriptionGetOptions { Expand = expandOptions };
1031-
var subscription = await subscriptionService.GetAsync(eventSubscription.Id, subscriptionGetOptions);
1032-
if (subscription == null)
1033-
{
1034-
throw new Exception("Subscription is null. " + eventSubscription.Id);
1035-
}
1036-
return subscription;
1037-
}
1038-
1039-
private async Task<Customer> GetCustomerAsync(string customerId)
1040-
{
1041-
if (string.IsNullOrWhiteSpace(customerId))
1042-
{
1043-
throw new Exception("Customer ID cannot be empty when attempting to get a customer from Stripe");
1044-
}
1045-
1046-
var customerService = new CustomerService();
1047-
var customer = await customerService.GetAsync(customerId);
1048-
if (customer == null)
1049-
{
1050-
throw new Exception($"Customer is null. {customerId}");
1051-
}
1052-
1053-
return customer;
1054-
}
1055-
1056-
private async Task<PaymentMethod> GetPaymentMethodAsync(Event parsedEvent, bool fresh = false,
1057-
List<string> expandOptions = null)
1058-
{
1059-
if (parsedEvent.Data.Object is not PaymentMethod eventPaymentMethod)
1060-
{
1061-
throw new Exception("Invoice is null (from parsed event). " + parsedEvent.Id);
1062-
}
1063-
1064-
if (!fresh)
1065-
{
1066-
return eventPaymentMethod;
1067-
}
1068-
1069-
var paymentMethodService = new PaymentMethodService();
1070-
var paymentMethodGetOptions = new PaymentMethodGetOptions { Expand = expandOptions };
1071-
var paymentMethod = await paymentMethodService.GetAsync(eventPaymentMethod.Id, paymentMethodGetOptions);
1072-
1073-
if (paymentMethod == null)
1074-
{
1075-
throw new Exception($"Payment method is null. {eventPaymentMethod.Id}");
1076-
}
1077-
1078-
return paymentMethod;
1079-
}
1080-
1081910
private async Task<Subscription> VerifyCorrectTaxRateForCharge(Invoice invoice, Subscription subscription)
1082911
{
1083912
if (!string.IsNullOrWhiteSpace(invoice?.CustomerAddress?.Country) && !string.IsNullOrWhiteSpace(invoice?.CustomerAddress?.PostalCode))

src/Core/Tools/Enums/ReferenceEventType.cs

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public enum ReferenceEventType
4242
OrganizationEditedByAdmin,
4343
[EnumMember(Value = "organization-created-by-admin")]
4444
OrganizationCreatedByAdmin,
45+
[EnumMember(Value = "organization-edited-in-stripe")]
46+
OrganizationEditedInStripe,
4547
[EnumMember(Value = "sm-service-account-accessed-secret")]
4648
SmServiceAccountAccessedSecret,
4749
}

0 commit comments

Comments
 (0)