Skip to content

Commit 04efe40

Browse files
[PM-28128] Create transaction for bank transfer charges (#6691)
* Create transaction for charges that were the result of a bank transfer * Claude feedback * Run dotnet format
1 parent 794240f commit 04efe40

6 files changed

Lines changed: 87 additions & 4 deletions

File tree

src/Billing/Services/IStripeEventUtilityService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public interface IStripeEventUtilityService
3636
/// <param name="userId"></param>
3737
/// /// <param name="providerId"></param>
3838
/// <returns></returns>
39-
Transaction FromChargeToTransaction(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId);
39+
Task<Transaction> FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId);
4040

4141
/// <summary>
4242
/// Attempts to pay the specified invoice. If a customer is eligible, the invoice is paid using Braintree or Stripe.

src/Billing/Services/IStripeFacade.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ Task<Customer> GetCustomer(
2020
RequestOptions requestOptions = null,
2121
CancellationToken cancellationToken = default);
2222

23+
IAsyncEnumerable<CustomerCashBalanceTransaction> GetCustomerCashBalanceTransactions(
24+
string customerId,
25+
CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null,
26+
RequestOptions requestOptions = null,
27+
CancellationToken cancellationToken = default);
28+
2329
Task<Customer> UpdateCustomer(
2430
string customerId,
2531
CustomerUpdateOptions customerUpdateOptions = null,

src/Billing/Services/Implementations/ChargeRefundedHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public async Task HandleAsync(Event parsedEvent)
3838
{
3939
// Attempt to create a transaction for the charge if it doesn't exist
4040
var (organizationId, userId, providerId) = await _stripeEventUtilityService.GetEntityIdsFromChargeAsync(charge);
41-
var tx = _stripeEventUtilityService.FromChargeToTransaction(charge, organizationId, userId, providerId);
41+
var tx = await _stripeEventUtilityService.FromChargeToTransactionAsync(charge, organizationId, userId, providerId);
4242
try
4343
{
4444
parentTransaction = await _transactionRepository.CreateAsync(tx);

src/Billing/Services/Implementations/ChargeSucceededHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public async Task HandleAsync(Event parsedEvent)
4646
return;
4747
}
4848

49-
var transaction = _stripeEventUtilityService.FromChargeToTransaction(charge, organizationId, userId, providerId);
49+
var transaction = await _stripeEventUtilityService.FromChargeToTransactionAsync(charge, organizationId, userId, providerId);
5050
if (!transaction.PaymentMethodType.HasValue)
5151
{
5252
_logger.LogWarning("Charge success from unsupported source/method. {ChargeId}", charge.Id);

src/Billing/Services/Implementations/StripeEventUtilityService.cs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public bool IsSponsoredSubscription(Subscription subscription) =>
124124
/// <param name="userId"></param>
125125
/// /// <param name="providerId"></param>
126126
/// <returns></returns>
127-
public Transaction FromChargeToTransaction(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId)
127+
public async Task<Transaction> FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId)
128128
{
129129
var transaction = new Transaction
130130
{
@@ -209,6 +209,24 @@ public Transaction FromChargeToTransaction(Charge charge, Guid? organizationId,
209209
transaction.PaymentMethodType = PaymentMethodType.BankAccount;
210210
transaction.Details = $"ACH => {achCreditTransfer.BankName}, {achCreditTransfer.AccountNumber}";
211211
}
212+
else if (charge.PaymentMethodDetails.CustomerBalance != null)
213+
{
214+
var bankTransferType = await GetFundingBankTransferTypeAsync(charge);
215+
216+
if (!string.IsNullOrEmpty(bankTransferType))
217+
{
218+
transaction.PaymentMethodType = PaymentMethodType.BankAccount;
219+
transaction.Details = bankTransferType switch
220+
{
221+
"eu_bank_transfer" => "EU Bank Transfer",
222+
"gb_bank_transfer" => "GB Bank Transfer",
223+
"jp_bank_transfer" => "JP Bank Transfer",
224+
"mx_bank_transfer" => "MX Bank Transfer",
225+
"us_bank_transfer" => "US Bank Transfer",
226+
_ => "Bank Transfer"
227+
};
228+
}
229+
}
212230

213231
break;
214232
}
@@ -406,4 +424,55 @@ private async Task<bool> AttemptToPayInvoiceWithStripeAsync(Invoice invoice)
406424
throw;
407425
}
408426
}
427+
428+
/// <summary>
429+
/// Retrieves the bank transfer type that funded a charge paid via customer balance.
430+
/// </summary>
431+
/// <param name="charge">The charge to analyze.</param>
432+
/// <returns>
433+
/// The bank transfer type (e.g., "us_bank_transfer", "eu_bank_transfer") if the charge was funded
434+
/// by a bank transfer via customer balance, otherwise null.
435+
/// </returns>
436+
private async Task<string> GetFundingBankTransferTypeAsync(Charge charge)
437+
{
438+
if (charge is not
439+
{
440+
CustomerId: not null,
441+
PaymentIntentId: not null,
442+
PaymentMethodDetails: { Type: "customer_balance" }
443+
})
444+
{
445+
return null;
446+
}
447+
448+
var cashBalanceTransactions = _stripeFacade.GetCustomerCashBalanceTransactions(charge.CustomerId);
449+
450+
string bankTransferType = null;
451+
var matchingPaymentIntentFound = false;
452+
453+
await foreach (var cashBalanceTransaction in cashBalanceTransactions)
454+
{
455+
switch (cashBalanceTransaction)
456+
{
457+
case { Type: "funded", Funded: not null }:
458+
{
459+
bankTransferType = cashBalanceTransaction.Funded.BankTransfer.Type;
460+
break;
461+
}
462+
case { Type: "applied_to_payment", AppliedToPayment: not null }
463+
when cashBalanceTransaction.AppliedToPayment.PaymentIntentId == charge.PaymentIntentId:
464+
{
465+
matchingPaymentIntentFound = true;
466+
break;
467+
}
468+
}
469+
470+
if (matchingPaymentIntentFound && !string.IsNullOrEmpty(bankTransferType))
471+
{
472+
return bankTransferType;
473+
}
474+
}
475+
476+
return null;
477+
}
409478
}

src/Billing/Services/Implementations/StripeFacade.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class StripeFacade : IStripeFacade
1111
{
1212
private readonly ChargeService _chargeService = new();
1313
private readonly CustomerService _customerService = new();
14+
private readonly CustomerCashBalanceTransactionService _customerCashBalanceTransactionService = new();
1415
private readonly EventService _eventService = new();
1516
private readonly InvoiceService _invoiceService = new();
1617
private readonly PaymentMethodService _paymentMethodService = new();
@@ -41,6 +42,13 @@ public async Task<Customer> GetCustomer(
4142
CancellationToken cancellationToken = default) =>
4243
await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken);
4344

45+
public IAsyncEnumerable<CustomerCashBalanceTransaction> GetCustomerCashBalanceTransactions(
46+
string customerId,
47+
CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null,
48+
RequestOptions requestOptions = null,
49+
CancellationToken cancellationToken = default)
50+
=> _customerCashBalanceTransactionService.ListAutoPagingAsync(customerId, customerCashBalanceTransactionListOptions, requestOptions, cancellationToken);
51+
4452
public async Task<Customer> UpdateCustomer(
4553
string customerId,
4654
CustomerUpdateOptions customerUpdateOptions = null,

0 commit comments

Comments
 (0)