1
0
mirror of https://github.com/bitwarden/server synced 2026-01-14 22:43:19 +00:00
Files
server/src/Billing/Services/Implementations/StripeEventService.cs
Alex Morask 3dd5accb56 [PM-24964] Stripe-hosted bank account verification (#6263)
* Implement bank account hosted URL verification with webhook handling notification

* Fix tests

* Run dotnet format

* Remove unused VerifyBankAccount operation

* Stephon's feedback

* Removing unused test

* TEMP: Add logging for deployment check

* Run dotnet format

* fix test

* Revert "fix test"

This reverts commit b8743ab3b5.

* Revert "Run dotnet format"

This reverts commit 5c861b0b72.

* Revert "TEMP: Add logging for deployment check"

This reverts commit 0a88acd6a1.

* Resolve GetPaymentMethodQuery order of operations
2025-09-09 12:22:42 -05:00

205 lines
7.1 KiB
C#

using Bit.Billing.Constants;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Stripe;
namespace Bit.Billing.Services.Implementations;
public class StripeEventService(
GlobalSettings globalSettings,
IOrganizationRepository organizationRepository,
IProviderRepository providerRepository,
ISetupIntentCache setupIntentCache,
IStripeFacade stripeFacade)
: IStripeEventService
{
public async Task<Charge> GetCharge(Event stripeEvent, bool fresh = false, List<string>? expand = null)
{
var charge = Extract<Charge>(stripeEvent);
if (!fresh)
{
return charge;
}
return await stripeFacade.GetCharge(charge.Id, new ChargeGetOptions { Expand = expand });
}
public async Task<Customer> GetCustomer(Event stripeEvent, bool fresh = false, List<string>? expand = null)
{
var customer = Extract<Customer>(stripeEvent);
if (!fresh)
{
return customer;
}
return await stripeFacade.GetCustomer(customer.Id, new CustomerGetOptions { Expand = expand });
}
public async Task<Invoice> GetInvoice(Event stripeEvent, bool fresh = false, List<string>? expand = null)
{
var invoice = Extract<Invoice>(stripeEvent);
if (!fresh)
{
return invoice;
}
return await stripeFacade.GetInvoice(invoice.Id, new InvoiceGetOptions { Expand = expand });
}
public async Task<PaymentMethod> GetPaymentMethod(Event stripeEvent, bool fresh = false,
List<string>? expand = null)
{
var paymentMethod = Extract<PaymentMethod>(stripeEvent);
if (!fresh)
{
return paymentMethod;
}
return await stripeFacade.GetPaymentMethod(paymentMethod.Id, new PaymentMethodGetOptions { Expand = expand });
}
public async Task<SetupIntent> GetSetupIntent(Event stripeEvent, bool fresh = false, List<string>? expand = null)
{
var setupIntent = Extract<SetupIntent>(stripeEvent);
if (!fresh)
{
return setupIntent;
}
return await stripeFacade.GetSetupIntent(setupIntent.Id, new SetupIntentGetOptions { Expand = expand });
}
public async Task<Subscription> GetSubscription(Event stripeEvent, bool fresh = false, List<string>? expand = null)
{
var subscription = Extract<Subscription>(stripeEvent);
if (!fresh)
{
return subscription;
}
return await stripeFacade.GetSubscription(subscription.Id, new SubscriptionGetOptions { Expand = expand });
}
public async Task<bool> ValidateCloudRegion(Event stripeEvent)
{
var serverRegion = globalSettings.BaseServiceUri.CloudRegion;
var customerExpansion = new List<string> { "customer" };
var customerMetadata = stripeEvent.Type switch
{
HandledStripeWebhook.SubscriptionDeleted or HandledStripeWebhook.SubscriptionUpdated =>
(await GetSubscription(stripeEvent, true, customerExpansion)).Customer?.Metadata,
HandledStripeWebhook.ChargeSucceeded or HandledStripeWebhook.ChargeRefunded =>
(await GetCharge(stripeEvent, true, customerExpansion)).Customer?.Metadata,
HandledStripeWebhook.UpcomingInvoice =>
await GetCustomerMetadataFromUpcomingInvoiceEvent(stripeEvent),
HandledStripeWebhook.PaymentSucceeded or HandledStripeWebhook.PaymentFailed
or HandledStripeWebhook.InvoiceCreated or HandledStripeWebhook.InvoiceFinalized =>
(await GetInvoice(stripeEvent, true, customerExpansion)).Customer?.Metadata,
HandledStripeWebhook.PaymentMethodAttached =>
(await GetPaymentMethod(stripeEvent, true, customerExpansion)).Customer?.Metadata,
HandledStripeWebhook.CustomerUpdated =>
(await GetCustomer(stripeEvent, true)).Metadata,
HandledStripeWebhook.SetupIntentSucceeded =>
await GetCustomerMetadataFromSetupIntentSucceededEvent(stripeEvent),
_ => null
};
if (customerMetadata == null)
{
return false;
}
var customerRegion = GetCustomerRegion(customerMetadata);
return customerRegion == serverRegion;
/* In Stripe, when we receive an invoice.upcoming event, the event does not include an Invoice ID because
the invoice hasn't been created yet. Therefore, rather than retrieving the fresh Invoice with a 'customer'
expansion, we need to use the Customer ID on the event to retrieve the metadata. */
async Task<Dictionary<string, string>?> GetCustomerMetadataFromUpcomingInvoiceEvent(Event localStripeEvent)
{
var invoice = await GetInvoice(localStripeEvent);
var customer = !string.IsNullOrEmpty(invoice.CustomerId)
? await stripeFacade.GetCustomer(invoice.CustomerId)
: null;
return customer?.Metadata;
}
async Task<Dictionary<string, string>?> GetCustomerMetadataFromSetupIntentSucceededEvent(Event localStripeEvent)
{
var setupIntent = await GetSetupIntent(localStripeEvent);
var subscriberId = await setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id);
if (subscriberId == null)
{
return null;
}
var organization = await organizationRepository.GetByIdAsync(subscriberId.Value);
if (organization is { GatewayCustomerId: not null })
{
var organizationCustomer = await stripeFacade.GetCustomer(organization.GatewayCustomerId);
return organizationCustomer.Metadata;
}
var provider = await providerRepository.GetByIdAsync(subscriberId.Value);
if (provider is not { GatewayCustomerId: not null })
{
return null;
}
var providerCustomer = await stripeFacade.GetCustomer(provider.GatewayCustomerId);
return providerCustomer.Metadata;
}
}
private static T Extract<T>(Event stripeEvent)
=> stripeEvent.Data.Object is not T type
? throw new Exception(
$"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{typeof(T).Name}'")
: type;
private static string GetCustomerRegion(IDictionary<string, string> customerMetadata)
{
const string defaultRegion = Core.Constants.CountryAbbreviations.UnitedStates;
if (customerMetadata.TryGetValue("region", out var value))
{
return value;
}
var incorrectlyCasedRegionKey = customerMetadata.Keys
.FirstOrDefault(key => key.Equals("region", StringComparison.OrdinalIgnoreCase));
if (incorrectlyCasedRegionKey is null)
{
return defaultRegion;
}
_ = customerMetadata.TryGetValue(incorrectlyCasedRegionKey, out var regionValue);
return !string.IsNullOrWhiteSpace(regionValue)
? regionValue
: defaultRegion;
}
}