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(