Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Billing/Services/IStripeEventUtilityService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public interface IStripeEventUtilityService
/// <param name="userId"></param>
/// /// <param name="providerId"></param>
/// <returns></returns>
Transaction FromChargeToTransaction(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId);
Task<Transaction> FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId);

/// <summary>
/// Attempts to pay the specified invoice. If a customer is eligible, the invoice is paid using Braintree or Stripe.
Expand Down
6 changes: 6 additions & 0 deletions src/Billing/Services/IStripeFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Task<Customer> GetCustomer(
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default);

IAsyncEnumerable<CustomerCashBalanceTransaction> GetCustomerCashBalanceTransactions(
string customerId,
CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null,
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default);

Task<Customer> UpdateCustomer(
string customerId,
CustomerUpdateOptions customerUpdateOptions = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public bool IsSponsoredSubscription(Subscription subscription) =>
/// <param name="userId"></param>
/// /// <param name="providerId"></param>
/// <returns></returns>
public Transaction FromChargeToTransaction(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId)
public async Task<Transaction> FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId)
{
var transaction = new Transaction
{
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -413,4 +431,55 @@ private async Task<bool> AttemptToPayInvoiceWithStripeAsync(Invoice invoice)
throw;
}
}

/// <summary>
/// Retrieves the bank transfer type that funded a charge paid via customer balance.
/// </summary>
/// <param name="charge">The charge to analyze.</param>
/// <returns>
/// 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.
/// </returns>
private async Task<string> 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;
}
}
8 changes: 8 additions & 0 deletions src/Billing/Services/Implementations/StripeFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -41,6 +42,13 @@ public async Task<Customer> GetCustomer(
CancellationToken cancellationToken = default) =>
await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken);

public IAsyncEnumerable<CustomerCashBalanceTransaction> GetCustomerCashBalanceTransactions(
string customerId,
CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null,
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default)
=> _customerCashBalanceTransactionService.ListAutoPagingAsync(customerId, customerCashBalanceTransactionListOptions, requestOptions, cancellationToken);

public async Task<Customer> UpdateCustomer(
string customerId,
CustomerUpdateOptions customerUpdateOptions = null,
Expand Down
Loading