From 052235bed63d53e2d7f8e7f8b45f1e7f0cc14955 Mon Sep 17 00:00:00 2001
From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Date: Wed, 20 Nov 2024 14:36:50 -0500
Subject: [PATCH] [PM-15048] Update bank account verification to use descriptor
code (#5048)
* Update verify bank account process to use descriptor code
* Run dotnet format
---
.../OrganizationBillingController.cs | 7 ++-
.../Requests/VerifyBankAccountRequestBody.cs | 6 +-
src/Core/Billing/BillingException.cs | 4 +-
src/Core/Billing/Constants/StripeConstants.cs | 3 +
.../Billing/Services/ISubscriberService.cs | 6 +-
.../Implementations/SubscriberService.cs | 55 ++++++++++++-------
.../Services/SubscriberServiceTests.cs | 13 ++---
7 files changed, 56 insertions(+), 38 deletions(-)
diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs
index b6a26f2404..7da0a0f602 100644
--- a/src/Api/Billing/Controllers/OrganizationBillingController.cs
+++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs
@@ -206,6 +206,11 @@ public class OrganizationBillingController(
return Error.Unauthorized();
}
+ if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM"))
+ {
+ return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'");
+ }
+
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
@@ -213,7 +218,7 @@ public class OrganizationBillingController(
return Error.NotFound();
}
- await subscriberService.VerifyBankAccount(organization, (requestBody.Amount1, requestBody.Amount2));
+ await subscriberService.VerifyBankAccount(organization, requestBody.DescriptorCode);
return TypedResults.Ok();
}
diff --git a/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs b/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs
index de98755f30..3e97d07a90 100644
--- a/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs
+++ b/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs
@@ -4,8 +4,6 @@ namespace Bit.Api.Billing.Models.Requests;
public class VerifyBankAccountRequestBody
{
- [Range(0, 99)]
- public long Amount1 { get; set; }
- [Range(0, 99)]
- public long Amount2 { get; set; }
+ [Required]
+ public string DescriptorCode { get; set; }
}
diff --git a/src/Core/Billing/BillingException.cs b/src/Core/Billing/BillingException.cs
index cdb3ce6b5a..c2b1b9f457 100644
--- a/src/Core/Billing/BillingException.cs
+++ b/src/Core/Billing/BillingException.cs
@@ -5,5 +5,7 @@ public class BillingException(
string message = null,
Exception innerException = null) : Exception(message, innerException)
{
- public string Response { get; } = response ?? "Something went wrong with your request. Please contact support.";
+ public const string DefaultMessage = "Something went wrong with your request. Please contact support.";
+
+ public string Response { get; } = response ?? DefaultMessage;
}
diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs
index 44cda35b70..7371b8b7e9 100644
--- a/src/Core/Billing/Constants/StripeConstants.cs
+++ b/src/Core/Billing/Constants/StripeConstants.cs
@@ -25,6 +25,9 @@ public static class StripeConstants
public static class ErrorCodes
{
public const string CustomerTaxLocationInvalid = "customer_tax_location_invalid";
+ public const string PaymentMethodMicroDepositVerificationAttemptsExceeded = "payment_method_microdeposit_verification_attempts_exceeded";
+ public const string PaymentMethodMicroDepositVerificationDescriptorCodeMismatch = "payment_method_microdeposit_verification_descriptor_code_mismatch";
+ public const string PaymentMethodMicroDepositVerificationTimeout = "payment_method_microdeposit_verification_timeout";
public const string TaxIdInvalid = "tax_id_invalid";
}
diff --git a/src/Core/Billing/Services/ISubscriberService.cs b/src/Core/Billing/Services/ISubscriberService.cs
index e7decd1cb2..bb0a23020c 100644
--- a/src/Core/Billing/Services/ISubscriberService.cs
+++ b/src/Core/Billing/Services/ISubscriberService.cs
@@ -141,13 +141,13 @@ public interface ISubscriberService
TaxInformation taxInformation);
///
- /// Verifies the subscriber's pending bank account using the provided .
+ /// Verifies the subscriber's pending bank account using the provided .
///
/// The subscriber to verify the bank account for.
- /// Deposits made to the subscriber's bank account in order to ensure they have access to it.
+ /// The code attached to a deposit made to the subscriber's bank account in order to ensure they have access to it.
/// Learn more.
///
Task VerifyBankAccount(
ISubscriber subscriber,
- (long, long) microdeposits);
+ string descriptorCode);
}
diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs
index b0d290a556..9b8f64be82 100644
--- a/src/Core/Billing/Services/Implementations/SubscriberService.cs
+++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs
@@ -3,6 +3,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
+using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
@@ -650,41 +651,53 @@ public class SubscriberService(
public async Task VerifyBankAccount(
ISubscriber subscriber,
- (long, long) microdeposits)
+ string descriptorCode)
{
- ArgumentNullException.ThrowIfNull(subscriber);
-
var setupIntentId = await setupIntentCache.Get(subscriber.Id);
if (string.IsNullOrEmpty(setupIntentId))
{
logger.LogError("No setup intent ID exists to verify for subscriber with ID ({SubscriberID})", subscriber.Id);
-
throw new BillingException();
}
- var (amount1, amount2) = microdeposits;
-
- await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId, new SetupIntentVerifyMicrodepositsOptions
+ try
{
- Amounts = [amount1, amount2]
- });
+ await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId,
+ new SetupIntentVerifyMicrodepositsOptions { DescriptorCode = descriptorCode });
- var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId);
+ var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId);
- await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId, new PaymentMethodAttachOptions
- {
- Customer = subscriber.GatewayCustomerId
- });
+ await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
+ new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId });
- await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId,
- new CustomerUpdateOptions
- {
- InvoiceSettings = new CustomerInvoiceSettingsOptions
+ await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId,
+ new CustomerUpdateOptions
{
- DefaultPaymentMethod = setupIntent.PaymentMethodId
- }
- });
+ InvoiceSettings = new CustomerInvoiceSettingsOptions
+ {
+ DefaultPaymentMethod = setupIntent.PaymentMethodId
+ }
+ });
+ }
+ catch (StripeException stripeException)
+ {
+ if (!string.IsNullOrEmpty(stripeException.StripeError?.Code))
+ {
+ var message = stripeException.StripeError.Code switch
+ {
+ StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationAttemptsExceeded => "You have exceeded the number of allowed verification attempts. Please contact support.",
+ StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationDescriptorCodeMismatch => "The verification code you provided does not match the one sent to your bank account. Please try again.",
+ StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationTimeout => "Your bank account was not verified within the required time period. Please contact support.",
+ _ => BillingException.DefaultMessage
+ };
+
+ throw new BadRequestException(message);
+ }
+
+ logger.LogError(stripeException, "An unhandled Stripe exception was thrown while verifying subscriber's ({SubscriberID}) bank account", subscriber.Id);
+ throw new BillingException();
+ }
}
#region Shared Utilities
diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs
index 652c22764f..385b185ffe 100644
--- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs
+++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs
@@ -1581,21 +1581,18 @@ public class SubscriberServiceTests
#region VerifyBankAccount
- [Theory, BitAutoData]
- public async Task VerifyBankAccount_NullSubscriber_ThrowsArgumentNullException(
- SutProvider sutProvider) => await Assert.ThrowsAsync(
- () => sutProvider.Sut.VerifyBankAccount(null, (0, 0)));
-
[Theory, BitAutoData]
public async Task VerifyBankAccount_NoSetupIntentId_ThrowsBillingException(
Provider provider,
- SutProvider sutProvider) => await ThrowsBillingExceptionAsync(() => sutProvider.Sut.VerifyBankAccount(provider, (1, 1)));
+ SutProvider sutProvider) => await ThrowsBillingExceptionAsync(() => sutProvider.Sut.VerifyBankAccount(provider, ""));
[Theory, BitAutoData]
public async Task VerifyBankAccount_MakesCorrectInvocations(
Provider provider,
SutProvider sutProvider)
{
+ const string descriptorCode = "SM1234";
+
var setupIntent = new SetupIntent
{
Id = "setup_intent_id",
@@ -1608,11 +1605,11 @@ public class SubscriberServiceTests
stripeAdapter.SetupIntentGet(setupIntent.Id).Returns(setupIntent);
- await sutProvider.Sut.VerifyBankAccount(provider, (1, 1));
+ await sutProvider.Sut.VerifyBankAccount(provider, descriptorCode);
await stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id,
Arg.Is(
- options => options.Amounts[0] == 1 && options.Amounts[1] == 1));
+ options => options.DescriptorCode == descriptorCode));
await stripeAdapter.Received(1).PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
Arg.Is(