1
0
mirror of https://github.com/bitwarden/server synced 2025-12-14 15:23:42 +00:00

Create transaction for charges that were the result of a bank transfer

This commit is contained in:
Alex Morask
2025-12-04 09:46:22 -06:00
parent 101ff9d6ed
commit c344a79338
6 changed files with 73 additions and 4 deletions

View File

@@ -36,7 +36,7 @@ public interface IStripeEventUtilityService
/// <param name="userId"></param> /// <param name="userId"></param>
/// /// <param name="providerId"></param> /// /// <param name="providerId"></param>
/// <returns></returns> /// <returns></returns>
Transaction FromChargeToTransaction(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId); Task<Transaction> FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId);
/// <summary> /// <summary>
/// Attempts to pay the specified invoice. If a customer is eligible, the invoice is paid using Braintree or Stripe. /// Attempts to pay the specified invoice. If a customer is eligible, the invoice is paid using Braintree or Stripe.

View File

@@ -20,6 +20,12 @@ public interface IStripeFacade
RequestOptions requestOptions = null, RequestOptions requestOptions = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
IAsyncEnumerable<CustomerCashBalanceTransaction> GetCustomerCashBalanceTransactions(
string customerId,
CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null,
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default);
Task<Customer> UpdateCustomer( Task<Customer> UpdateCustomer(
string customerId, string customerId,
CustomerUpdateOptions customerUpdateOptions = null, CustomerUpdateOptions customerUpdateOptions = null,

View File

@@ -38,7 +38,7 @@ public class ChargeRefundedHandler : IChargeRefundedHandler
{ {
// Attempt to create a transaction for the charge if it doesn't exist // Attempt to create a transaction for the charge if it doesn't exist
var (organizationId, userId, providerId) = await _stripeEventUtilityService.GetEntityIdsFromChargeAsync(charge); 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 try
{ {
parentTransaction = await _transactionRepository.CreateAsync(tx); parentTransaction = await _transactionRepository.CreateAsync(tx);

View File

@@ -46,7 +46,7 @@ public class ChargeSucceededHandler : IChargeSucceededHandler
return; return;
} }
var transaction = _stripeEventUtilityService.FromChargeToTransaction(charge, organizationId, userId, providerId); var transaction = await _stripeEventUtilityService.FromChargeToTransactionAsync(charge, organizationId, userId, providerId);
if (!transaction.PaymentMethodType.HasValue) if (!transaction.PaymentMethodType.HasValue)
{ {
_logger.LogWarning("Charge success from unsupported source/method. {ChargeId}", charge.Id); _logger.LogWarning("Charge success from unsupported source/method. {ChargeId}", charge.Id);

View File

@@ -124,7 +124,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService
/// <param name="userId"></param> /// <param name="userId"></param>
/// /// <param name="providerId"></param> /// /// <param name="providerId"></param>
/// <returns></returns> /// <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 var transaction = new Transaction
{ {
@@ -209,6 +209,24 @@ public class StripeEventUtilityService : IStripeEventUtilityService
transaction.PaymentMethodType = PaymentMethodType.BankAccount; transaction.PaymentMethodType = PaymentMethodType.BankAccount;
transaction.Details = $"ACH => {achCreditTransfer.BankName}, {achCreditTransfer.AccountNumber}"; 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; break;
} }
@@ -413,4 +431,41 @@ public class StripeEventUtilityService : IStripeEventUtilityService
throw; throw;
} }
} }
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 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;
}
} }

View File

@@ -11,6 +11,7 @@ public class StripeFacade : IStripeFacade
{ {
private readonly ChargeService _chargeService = new(); private readonly ChargeService _chargeService = new();
private readonly CustomerService _customerService = new(); private readonly CustomerService _customerService = new();
private readonly CustomerCashBalanceTransactionService _customerCashBalanceTransactionService = new();
private readonly EventService _eventService = new(); private readonly EventService _eventService = new();
private readonly InvoiceService _invoiceService = new(); private readonly InvoiceService _invoiceService = new();
private readonly PaymentMethodService _paymentMethodService = new(); private readonly PaymentMethodService _paymentMethodService = new();
@@ -41,6 +42,13 @@ public class StripeFacade : IStripeFacade
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken); await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken);
public IAsyncEnumerable<CustomerCashBalanceTransaction> GetCustomerCashBalanceTransactions(
string customerId,
CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions,
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default)
=> _customerCashBalanceTransactionService.ListAutoPagingAsync(customerId, customerCashBalanceTransactionListOptions, requestOptions, cancellationToken);
public async Task<Customer> UpdateCustomer( public async Task<Customer> UpdateCustomer(
string customerId, string customerId,
CustomerUpdateOptions customerUpdateOptions = null, CustomerUpdateOptions customerUpdateOptions = null,