diff --git a/src/Billing/Services/IStripeEventUtilityService.cs b/src/Billing/Services/IStripeEventUtilityService.cs
index a5f536ad111b..058f56c887b5 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 f821eeed5f2e..c7073b9cf936 100644
--- a/src/Billing/Services/IStripeFacade.cs
+++ b/src/Billing/Services/IStripeFacade.cs
@@ -20,6 +20,12 @@ Task GetCustomer(
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 905491b6c580..8cc3cb2ce670 100644
--- a/src/Billing/Services/Implementations/ChargeRefundedHandler.cs
+++ b/src/Billing/Services/Implementations/ChargeRefundedHandler.cs
@@ -38,7 +38,7 @@ public async Task HandleAsync(Event parsedEvent)
{
// 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 bd8ea7def21b..20c4dcfa98c7 100644
--- a/src/Billing/Services/Implementations/ChargeSucceededHandler.cs
+++ b/src/Billing/Services/Implementations/ChargeSucceededHandler.cs
@@ -46,7 +46,7 @@ public async Task HandleAsync(Event parsedEvent)
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 06a5d8a8902b..4d5a077e261d 100644
--- a/src/Billing/Services/Implementations/StripeEventUtilityService.cs
+++ b/src/Billing/Services/Implementations/StripeEventUtilityService.cs
@@ -124,7 +124,7 @@ public bool IsSponsoredSubscription(Subscription subscription) =>
///
/// ///
///
- 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 Transaction FromChargeToTransaction(Charge charge, Guid? organizationId,
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,55 @@ private async Task AttemptToPayInvoiceWithStripeAsync(Invoice invoice)
throw;
}
}
+
+ ///
+ /// Retrieves the bank transfer type that funded a charge paid via customer balance.
+ ///
+ /// The charge to analyze.
+ ///
+ /// The bank transfer type (e.g., "us_bank_transfer", "eu_bank_transfer") if the charge was funded
+ /// by a bank transfer via customer balance, otherwise null.
+ ///
+ 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 matchingPaymentIntentFound = 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 }
+ when cashBalanceTransaction.AppliedToPayment.PaymentIntentId == charge.PaymentIntentId:
+ {
+ matchingPaymentIntentFound = true;
+ break;
+ }
+ }
+
+ if (matchingPaymentIntentFound && !string.IsNullOrEmpty(bankTransferType))
+ {
+ return bankTransferType;
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/Billing/Services/Implementations/StripeFacade.cs b/src/Billing/Services/Implementations/StripeFacade.cs
index bb72091bc69d..49cde981cde1 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 async Task GetCustomer(
CancellationToken cancellationToken = default) =>
await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken);
+ public IAsyncEnumerable GetCustomerCashBalanceTransactions(
+ string customerId,
+ CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null,
+ RequestOptions requestOptions = null,
+ CancellationToken cancellationToken = default)
+ => _customerCashBalanceTransactionService.ListAutoPagingAsync(customerId, customerCashBalanceTransactionListOptions, requestOptions, cancellationToken);
+
public async Task UpdateCustomer(
string customerId,
CustomerUpdateOptions customerUpdateOptions = null,