mirror of
https://github.com/bitwarden/server
synced 2026-01-14 22:43:19 +00:00
* 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 commitb8743ab3b5. * Revert "Run dotnet format" This reverts commit5c861b0b72. * Revert "TEMP: Add logging for deployment check" This reverts commit0a88acd6a1. * Resolve GetPaymentMethodQuery order of operations
205 lines
7.1 KiB
C#
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;
|
|
}
|
|
}
|