diff --git a/src/Billing/Services/IStripeEventUtilityService.cs b/src/Billing/Services/IStripeEventUtilityService.cs
index a5f536ad11..058f56c887 100644
--- a/src/Billing/Services/IStripeEventUtilityService.cs
+++ b/src/Billing/Services/IStripeEventUtilityService.cs
@@ -36,7 +36,7 @@ public interface IStripeEventUtilityService
///
/// ///
///
- Transaction FromChargeToTransaction(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId);
+ Task FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId);
///
/// Attempts to pay the specified invoice. If a customer is eligible, the invoice is paid using Braintree or Stripe.
diff --git a/src/Billing/Services/IStripeFacade.cs b/src/Billing/Services/IStripeFacade.cs
index f821eeed5f..c7073b9cf9 100644
--- a/src/Billing/Services/IStripeFacade.cs
+++ b/src/Billing/Services/IStripeFacade.cs
@@ -20,6 +20,12 @@ public interface IStripeFacade
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default);
+ IAsyncEnumerable GetCustomerCashBalanceTransactions(
+ string customerId,
+ CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null,
+ RequestOptions requestOptions = null,
+ CancellationToken cancellationToken = default);
+
Task UpdateCustomer(
string customerId,
CustomerUpdateOptions customerUpdateOptions = null,
diff --git a/src/Billing/Services/Implementations/ChargeRefundedHandler.cs b/src/Billing/Services/Implementations/ChargeRefundedHandler.cs
index 905491b6c5..8cc3cb2ce6 100644
--- a/src/Billing/Services/Implementations/ChargeRefundedHandler.cs
+++ b/src/Billing/Services/Implementations/ChargeRefundedHandler.cs
@@ -38,7 +38,7 @@ public class ChargeRefundedHandler : IChargeRefundedHandler
{
// Attempt to create a transaction for the charge if it doesn't exist
var (organizationId, userId, providerId) = await _stripeEventUtilityService.GetEntityIdsFromChargeAsync(charge);
- var tx = _stripeEventUtilityService.FromChargeToTransaction(charge, organizationId, userId, providerId);
+ var tx = await _stripeEventUtilityService.FromChargeToTransactionAsync(charge, organizationId, userId, providerId);
try
{
parentTransaction = await _transactionRepository.CreateAsync(tx);
diff --git a/src/Billing/Services/Implementations/ChargeSucceededHandler.cs b/src/Billing/Services/Implementations/ChargeSucceededHandler.cs
index bd8ea7def2..20c4dcfa98 100644
--- a/src/Billing/Services/Implementations/ChargeSucceededHandler.cs
+++ b/src/Billing/Services/Implementations/ChargeSucceededHandler.cs
@@ -46,7 +46,7 @@ public class ChargeSucceededHandler : IChargeSucceededHandler
return;
}
- var transaction = _stripeEventUtilityService.FromChargeToTransaction(charge, organizationId, userId, providerId);
+ var transaction = await _stripeEventUtilityService.FromChargeToTransactionAsync(charge, organizationId, userId, providerId);
if (!transaction.PaymentMethodType.HasValue)
{
_logger.LogWarning("Charge success from unsupported source/method. {ChargeId}", charge.Id);
diff --git a/src/Billing/Services/Implementations/StripeEventUtilityService.cs b/src/Billing/Services/Implementations/StripeEventUtilityService.cs
index 06a5d8a890..5dbdd7a0a0 100644
--- a/src/Billing/Services/Implementations/StripeEventUtilityService.cs
+++ b/src/Billing/Services/Implementations/StripeEventUtilityService.cs
@@ -124,7 +124,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService
///
/// ///
///
- public Transaction FromChargeToTransaction(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId)
+ public async Task FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId)
{
var transaction = new Transaction
{
@@ -209,6 +209,24 @@ public class StripeEventUtilityService : IStripeEventUtilityService
transaction.PaymentMethodType = PaymentMethodType.BankAccount;
transaction.Details = $"ACH => {achCreditTransfer.BankName}, {achCreditTransfer.AccountNumber}";
}
+ else if (charge.PaymentMethodDetails.CustomerBalance != null)
+ {
+ var bankTransferType = await GetFundingBankTransferTypeAsync(charge);
+
+ if (!string.IsNullOrEmpty(bankTransferType))
+ {
+ transaction.PaymentMethodType = PaymentMethodType.BankAccount;
+ transaction.Details = bankTransferType switch
+ {
+ "eu_bank_transfer" => "EU Bank Transfer",
+ "gb_bank_transfer" => "GB Bank Transfer",
+ "jp_bank_transfer" => "JP Bank Transfer",
+ "mx_bank_transfer" => "MX Bank Transfer",
+ "us_bank_transfer" => "US Bank Transfer",
+ _ => "Bank Transfer"
+ };
+ }
+ }
break;
}
@@ -413,4 +431,41 @@ public class StripeEventUtilityService : IStripeEventUtilityService
throw;
}
}
+
+ private async Task GetFundingBankTransferTypeAsync(Charge charge)
+ {
+ if (charge is not
+ {
+ CustomerId: not null,
+ PaymentIntentId: not null,
+ PaymentMethodDetails: { Type: "customer_balance" }
+ })
+ {
+ return null;
+ }
+
+ var cashBalanceTransactions = _stripeFacade.GetCustomerCashBalanceTransactions(charge.CustomerId);
+
+ string bankTransferType = null;
+ var fundedCharge = false;
+
+ await foreach (var cashBalanceTransaction in cashBalanceTransactions)
+ {
+ switch (cashBalanceTransaction)
+ {
+ case { Type: "funded", Funded: not null }:
+ {
+ bankTransferType = cashBalanceTransaction.Funded.BankTransfer.Type;
+ break;
+ }
+ case { Type: "applied_to_payment", AppliedToPayment: not null }:
+ {
+ fundedCharge = charge.PaymentIntentId == cashBalanceTransaction.AppliedToPayment.PaymentIntentId;
+ break;
+ }
+ }
+ }
+
+ return !fundedCharge ? null : bankTransferType;
+ }
}
diff --git a/src/Billing/Services/Implementations/StripeFacade.cs b/src/Billing/Services/Implementations/StripeFacade.cs
index bb72091bc6..eeb7b86233 100644
--- a/src/Billing/Services/Implementations/StripeFacade.cs
+++ b/src/Billing/Services/Implementations/StripeFacade.cs
@@ -11,6 +11,7 @@ public class StripeFacade : IStripeFacade
{
private readonly ChargeService _chargeService = new();
private readonly CustomerService _customerService = new();
+ private readonly CustomerCashBalanceTransactionService _customerCashBalanceTransactionService = new();
private readonly EventService _eventService = new();
private readonly InvoiceService _invoiceService = new();
private readonly PaymentMethodService _paymentMethodService = new();
@@ -41,6 +42,13 @@ public class StripeFacade : IStripeFacade
CancellationToken cancellationToken = default) =>
await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken);
+ public IAsyncEnumerable GetCustomerCashBalanceTransactions(
+ string customerId,
+ CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions,
+ RequestOptions requestOptions = null,
+ CancellationToken cancellationToken = default)
+ => _customerCashBalanceTransactionService.ListAutoPagingAsync(customerId, customerCashBalanceTransactionListOptions, requestOptions, cancellationToken);
+
public async Task UpdateCustomer(
string customerId,
CustomerUpdateOptions customerUpdateOptions = null,