mirror of
https://github.com/bitwarden/server
synced 2026-02-20 19:33:32 +00:00
[PM-31040] Replace ISetupIntentCache with customer-based approach (#6954)
* docs(billing): add design document for replacing SetupIntent cache * docs(billing): add implementation plan for replacing SetupIntent cache * feat(db): add gateway lookup stored procedures for Organization, Provider, and User * feat(db): add gateway lookup indexes to Organization, Provider, and User table definitions * chore(db): add SQL Server migration for gateway lookup indexes and stored procedures * feat(repos): add gateway lookup methods to IOrganizationRepository and Dapper implementation * feat(repos): add gateway lookup methods to IProviderRepository and Dapper implementation * feat(repos): add gateway lookup methods to IUserRepository and Dapper implementation * feat(repos): add EF OrganizationRepository gateway lookup methods and index configuration * feat(repos): add EF ProviderRepository gateway lookup methods and index configuration * feat(repos): add EF UserRepository gateway lookup methods and index configuration * chore(db): add EF migrations for gateway lookup indexes * refactor(billing): update SetupIntentSucceededHandler to use repository instead of cache * refactor(billing): simplify StripeEventService by expanding customer on SetupIntent * refactor(billing): query Stripe for SetupIntents by customer ID in GetPaymentMethodQuery * refactor(billing): query Stripe for SetupIntents by customer ID in HasPaymentMethodQuery * refactor(billing): update OrganizationBillingService to set customer on SetupIntent * refactor(billing): update ProviderBillingService to set customer on SetupIntent and query by customer * refactor(billing): update UpdatePaymentMethodCommand to set customer on SetupIntent * refactor(billing): remove bank account support from CreatePremiumCloudHostedSubscriptionCommand * refactor(billing): remove OrganizationBillingService.UpdatePaymentMethod dead code * refactor(billing): remove ProviderBillingService.UpdatePaymentMethod * refactor(billing): remove PremiumUserBillingService.UpdatePaymentMethod and UserService.ReplacePaymentMethodAsync * refactor(billing): remove SubscriberService.UpdatePaymentSource and related dead code * refactor(billing): update SubscriberService.GetPaymentSourceAsync to query Stripe by customer ID Add Task 15a to plan - this was a missed requirement for updating GetPaymentSourceAsync which still used the cache. * refactor(billing): complete removal of PremiumUserBillingService.Finalize and UserService.SignUpPremiumAsync * refactor(billing): remove ISetupIntentCache and SetupIntentDistributedCache * chore: remove temporary planning documents * chore: run dotnet format * fix(billing): add MaxLength(50) to Provider gateway ID properties * chore(db): add EF migrations for Provider gateway column lengths * chore: run dotnet format * chore: rename SQL migration for chronological order
This commit is contained in:
@@ -3,9 +3,9 @@ using Bit.Billing.Services.Implementations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -18,28 +18,28 @@ public class SetupIntentSucceededHandlerTests
|
||||
private static readonly Event _mockEvent = new() { Id = "evt_test", Type = "setup_intent.succeeded" };
|
||||
private static readonly string[] _expand = ["payment_method"];
|
||||
|
||||
private readonly ILogger<SetupIntentSucceededHandler> _logger;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IPushNotificationAdapter _pushNotificationAdapter;
|
||||
private readonly ISetupIntentCache _setupIntentCache;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IStripeEventService _stripeEventService;
|
||||
private readonly SetupIntentSucceededHandler _handler;
|
||||
|
||||
public SetupIntentSucceededHandlerTests()
|
||||
{
|
||||
_logger = Substitute.For<ILogger<SetupIntentSucceededHandler>>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_providerRepository = Substitute.For<IProviderRepository>();
|
||||
_pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();
|
||||
_setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||
_stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
_stripeEventService = Substitute.For<IStripeEventService>();
|
||||
|
||||
_handler = new SetupIntentSucceededHandler(
|
||||
_logger,
|
||||
_organizationRepository,
|
||||
_providerRepository,
|
||||
_pushNotificationAdapter,
|
||||
_setupIntentCache,
|
||||
_stripeAdapter,
|
||||
_stripeEventService);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ public class SetupIntentSucceededHandlerTests
|
||||
await _handler.HandleAsync(_mockEvent);
|
||||
|
||||
// Assert
|
||||
await _setupIntentCache.DidNotReceiveWithAnyArgs().GetSubscriberIdForSetupIntent(Arg.Any<string>());
|
||||
await _organizationRepository.DidNotReceiveWithAnyArgs().GetByGatewayCustomerIdAsync(Arg.Any<string>());
|
||||
await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(
|
||||
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
|
||||
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
|
||||
@@ -68,10 +68,10 @@ public class SetupIntentSucceededHandlerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_NoSubscriberIdInCache_Returns()
|
||||
public async Task HandleAsync_NoCustomerIdOnSetupIntent_Returns()
|
||||
{
|
||||
// Arrange
|
||||
var setupIntent = CreateSetupIntent();
|
||||
var setupIntent = CreateSetupIntent(customerId: null);
|
||||
|
||||
_stripeEventService.GetSetupIntent(
|
||||
_mockEvent,
|
||||
@@ -79,8 +79,35 @@ public class SetupIntentSucceededHandlerTests
|
||||
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||
.Returns(setupIntent);
|
||||
|
||||
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
|
||||
.Returns((Guid?)null);
|
||||
// Act
|
||||
await _handler.HandleAsync(_mockEvent);
|
||||
|
||||
// Assert
|
||||
await _organizationRepository.DidNotReceiveWithAnyArgs().GetByGatewayCustomerIdAsync(Arg.Any<string>());
|
||||
await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(
|
||||
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
|
||||
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
|
||||
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_NoOrganizationOrProviderFound_LogsErrorAndReturns()
|
||||
{
|
||||
// Arrange
|
||||
var customerId = "cus_test";
|
||||
var setupIntent = CreateSetupIntent(customerId: customerId);
|
||||
|
||||
_stripeEventService.GetSetupIntent(
|
||||
_mockEvent,
|
||||
true,
|
||||
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||
.Returns(setupIntent);
|
||||
|
||||
_organizationRepository.GetByGatewayCustomerIdAsync(customerId)
|
||||
.Returns((Organization?)null);
|
||||
|
||||
_providerRepository.GetByGatewayCustomerIdAsync(customerId)
|
||||
.Returns((Provider?)null);
|
||||
|
||||
// Act
|
||||
await _handler.HandleAsync(_mockEvent);
|
||||
@@ -96,9 +123,9 @@ public class SetupIntentSucceededHandlerTests
|
||||
public async Task HandleAsync_ValidOrganization_AttachesPaymentMethodAndSendsNotification()
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var organization = new Organization { Id = organizationId, Name = "Test Org", GatewayCustomerId = "cus_test" };
|
||||
var setupIntent = CreateSetupIntent();
|
||||
var customerId = "cus_test";
|
||||
var organization = new Organization { Id = Guid.NewGuid(), Name = "Test Org", GatewayCustomerId = customerId };
|
||||
var setupIntent = CreateSetupIntent(customerId: customerId);
|
||||
|
||||
_stripeEventService.GetSetupIntent(
|
||||
_mockEvent,
|
||||
@@ -106,10 +133,7 @@ public class SetupIntentSucceededHandlerTests
|
||||
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||
.Returns(setupIntent);
|
||||
|
||||
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
|
||||
.Returns(organizationId);
|
||||
|
||||
_organizationRepository.GetByIdAsync(organizationId)
|
||||
_organizationRepository.GetByGatewayCustomerIdAsync(customerId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
@@ -122,15 +146,18 @@ public class SetupIntentSucceededHandlerTests
|
||||
|
||||
await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(organization);
|
||||
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
|
||||
|
||||
// Provider should not be queried when organization is found
|
||||
await _providerRepository.DidNotReceiveWithAnyArgs().GetByGatewayCustomerIdAsync(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_ValidProvider_AttachesPaymentMethodAndSendsNotification()
|
||||
{
|
||||
// Arrange
|
||||
var providerId = Guid.NewGuid();
|
||||
var provider = new Provider { Id = providerId, Name = "Test Provider", GatewayCustomerId = "cus_test" };
|
||||
var setupIntent = CreateSetupIntent();
|
||||
var customerId = "cus_test";
|
||||
var provider = new Provider { Id = Guid.NewGuid(), Name = "Test Provider", GatewayCustomerId = customerId };
|
||||
var setupIntent = CreateSetupIntent(customerId: customerId);
|
||||
|
||||
_stripeEventService.GetSetupIntent(
|
||||
_mockEvent,
|
||||
@@ -138,13 +165,10 @@ public class SetupIntentSucceededHandlerTests
|
||||
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||
.Returns(setupIntent);
|
||||
|
||||
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
|
||||
.Returns(providerId);
|
||||
|
||||
_organizationRepository.GetByIdAsync(providerId)
|
||||
_organizationRepository.GetByGatewayCustomerIdAsync(customerId)
|
||||
.Returns((Organization?)null);
|
||||
|
||||
_providerRepository.GetByIdAsync(providerId)
|
||||
_providerRepository.GetByGatewayCustomerIdAsync(customerId)
|
||||
.Returns(provider);
|
||||
|
||||
// Act
|
||||
@@ -163,9 +187,9 @@ public class SetupIntentSucceededHandlerTests
|
||||
public async Task HandleAsync_OrganizationWithoutGatewayCustomerId_DoesNotAttachPaymentMethod()
|
||||
{
|
||||
// Arrange
|
||||
var organizationId = Guid.NewGuid();
|
||||
var organization = new Organization { Id = organizationId, Name = "Test Org", GatewayCustomerId = null };
|
||||
var setupIntent = CreateSetupIntent();
|
||||
var customerId = "cus_test";
|
||||
var organization = new Organization { Id = Guid.NewGuid(), Name = "Test Org", GatewayCustomerId = null };
|
||||
var setupIntent = CreateSetupIntent(customerId: customerId);
|
||||
|
||||
_stripeEventService.GetSetupIntent(
|
||||
_mockEvent,
|
||||
@@ -173,10 +197,7 @@ public class SetupIntentSucceededHandlerTests
|
||||
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||
.Returns(setupIntent);
|
||||
|
||||
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
|
||||
.Returns(organizationId);
|
||||
|
||||
_organizationRepository.GetByIdAsync(organizationId)
|
||||
_organizationRepository.GetByGatewayCustomerIdAsync(customerId)
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
@@ -193,9 +214,9 @@ public class SetupIntentSucceededHandlerTests
|
||||
public async Task HandleAsync_ProviderWithoutGatewayCustomerId_DoesNotAttachPaymentMethod()
|
||||
{
|
||||
// Arrange
|
||||
var providerId = Guid.NewGuid();
|
||||
var provider = new Provider { Id = providerId, Name = "Test Provider", GatewayCustomerId = null };
|
||||
var setupIntent = CreateSetupIntent();
|
||||
var customerId = "cus_test";
|
||||
var provider = new Provider { Id = Guid.NewGuid(), Name = "Test Provider", GatewayCustomerId = null };
|
||||
var setupIntent = CreateSetupIntent(customerId: customerId);
|
||||
|
||||
_stripeEventService.GetSetupIntent(
|
||||
_mockEvent,
|
||||
@@ -203,13 +224,10 @@ public class SetupIntentSucceededHandlerTests
|
||||
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||
.Returns(setupIntent);
|
||||
|
||||
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
|
||||
.Returns(providerId);
|
||||
|
||||
_organizationRepository.GetByIdAsync(providerId)
|
||||
_organizationRepository.GetByGatewayCustomerIdAsync(customerId)
|
||||
.Returns((Organization?)null);
|
||||
|
||||
_providerRepository.GetByIdAsync(providerId)
|
||||
_providerRepository.GetByGatewayCustomerIdAsync(customerId)
|
||||
.Returns(provider);
|
||||
|
||||
// Act
|
||||
@@ -222,7 +240,7 @@ public class SetupIntentSucceededHandlerTests
|
||||
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
|
||||
}
|
||||
|
||||
private static SetupIntent CreateSetupIntent(bool hasUSBankAccount = true)
|
||||
private static SetupIntent CreateSetupIntent(bool hasUSBankAccount = true, string? customerId = "cus_default")
|
||||
{
|
||||
var paymentMethod = new PaymentMethod
|
||||
{
|
||||
@@ -234,6 +252,7 @@ public class SetupIntentSucceededHandlerTests
|
||||
var setupIntent = new SetupIntent
|
||||
{
|
||||
Id = "seti_test",
|
||||
CustomerId = customerId,
|
||||
PaymentMethod = paymentMethod
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Billing.Services.Implementations;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -13,9 +9,6 @@ namespace Bit.Billing.Test.Services;
|
||||
|
||||
public class StripeEventServiceTests
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly ISetupIntentCache _setupIntentCache;
|
||||
private readonly IStripeFacade _stripeFacade;
|
||||
private readonly StripeEventService _stripeEventService;
|
||||
|
||||
@@ -25,16 +18,9 @@ public class StripeEventServiceTests
|
||||
var baseServiceUriSettings = new GlobalSettings.BaseServiceUriSettings(globalSettings) { CloudRegion = "US" };
|
||||
globalSettings.BaseServiceUri = baseServiceUriSettings;
|
||||
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_providerRepository = Substitute.For<IProviderRepository>();
|
||||
_setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||
_stripeFacade = Substitute.For<IStripeFacade>();
|
||||
_stripeEventService = new StripeEventService(
|
||||
globalSettings,
|
||||
Substitute.For<ILogger<StripeEventService>>(),
|
||||
_organizationRepository,
|
||||
_providerRepository,
|
||||
_setupIntentCache,
|
||||
_stripeFacade);
|
||||
}
|
||||
|
||||
@@ -658,29 +644,17 @@ public class StripeEventServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateCloudRegion_SetupIntentSucceeded_OrganizationCustomer_Success()
|
||||
public async Task ValidateCloudRegion_SetupIntentSucceeded_WithCustomer_Success()
|
||||
{
|
||||
// Arrange
|
||||
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
|
||||
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||
var organizationId = Guid.NewGuid();
|
||||
var organizationCustomerId = "cus_org_test";
|
||||
|
||||
var mockOrganization = new Core.AdminConsole.Entities.Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
GatewayCustomerId = organizationCustomerId
|
||||
};
|
||||
var customer = CreateMockCustomer();
|
||||
var mockSetupIntent = new SetupIntent { Id = "seti_test", Customer = customer };
|
||||
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||
|
||||
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
|
||||
.Returns(organizationId);
|
||||
|
||||
_organizationRepository.GetByIdAsync(organizationId)
|
||||
.Returns(mockOrganization);
|
||||
|
||||
_stripeFacade.GetCustomer(organizationCustomerId)
|
||||
.Returns(customer);
|
||||
_stripeFacade.GetSetupIntent(
|
||||
mockSetupIntent.Id,
|
||||
Arg.Any<SetupIntentGetOptions>())
|
||||
.Returns(mockSetupIntent);
|
||||
|
||||
// Act
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
@@ -688,60 +662,22 @@ public class StripeEventServiceTests
|
||||
// Assert
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
|
||||
await _organizationRepository.Received(1).GetByIdAsync(organizationId);
|
||||
await _stripeFacade.Received(1).GetCustomer(organizationCustomerId);
|
||||
await _stripeFacade.Received(1).GetSetupIntent(
|
||||
mockSetupIntent.Id,
|
||||
Arg.Any<SetupIntentGetOptions>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateCloudRegion_SetupIntentSucceeded_ProviderCustomer_Success()
|
||||
public async Task ValidateCloudRegion_SetupIntentSucceeded_NoCustomer_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
|
||||
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||
var providerId = Guid.NewGuid();
|
||||
var providerCustomerId = "cus_provider_test";
|
||||
|
||||
var mockProvider = new Core.AdminConsole.Entities.Provider.Provider
|
||||
{
|
||||
Id = providerId,
|
||||
GatewayCustomerId = providerCustomerId
|
||||
};
|
||||
var customer = CreateMockCustomer();
|
||||
|
||||
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
|
||||
.Returns(providerId);
|
||||
|
||||
_organizationRepository.GetByIdAsync(providerId)
|
||||
.Returns((Core.AdminConsole.Entities.Organization?)null);
|
||||
|
||||
_providerRepository.GetByIdAsync(providerId)
|
||||
.Returns(mockProvider);
|
||||
|
||||
_stripeFacade.GetCustomer(providerCustomerId)
|
||||
.Returns(customer);
|
||||
|
||||
// Act
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
|
||||
await _organizationRepository.Received(1).GetByIdAsync(providerId);
|
||||
await _providerRepository.Received(1).GetByIdAsync(providerId);
|
||||
await _stripeFacade.Received(1).GetCustomer(providerCustomerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateCloudRegion_SetupIntentSucceeded_NoSubscriberIdInCache_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
|
||||
var mockSetupIntent = new SetupIntent { Id = "seti_test", Customer = null };
|
||||
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||
|
||||
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
|
||||
.Returns((Guid?)null);
|
||||
_stripeFacade.GetSetupIntent(
|
||||
mockSetupIntent.Id,
|
||||
Arg.Any<SetupIntentGetOptions>())
|
||||
.Returns(mockSetupIntent);
|
||||
|
||||
// Act
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
@@ -749,91 +685,9 @@ public class StripeEventServiceTests
|
||||
// Assert
|
||||
Assert.False(cloudRegionValid);
|
||||
|
||||
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
|
||||
await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await _providerRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await _stripeFacade.DidNotReceive().GetCustomer(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateCloudRegion_SetupIntentSucceeded_OrganizationWithoutGatewayCustomerId_ChecksProvider()
|
||||
{
|
||||
// Arrange
|
||||
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
|
||||
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||
var subscriberId = Guid.NewGuid();
|
||||
var providerCustomerId = "cus_provider_test";
|
||||
|
||||
var mockOrganizationWithoutCustomerId = new Core.AdminConsole.Entities.Organization
|
||||
{
|
||||
Id = subscriberId,
|
||||
GatewayCustomerId = null
|
||||
};
|
||||
|
||||
var mockProvider = new Core.AdminConsole.Entities.Provider.Provider
|
||||
{
|
||||
Id = subscriberId,
|
||||
GatewayCustomerId = providerCustomerId
|
||||
};
|
||||
var customer = CreateMockCustomer();
|
||||
|
||||
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
|
||||
.Returns(subscriberId);
|
||||
|
||||
_organizationRepository.GetByIdAsync(subscriberId)
|
||||
.Returns(mockOrganizationWithoutCustomerId);
|
||||
|
||||
_providerRepository.GetByIdAsync(subscriberId)
|
||||
.Returns(mockProvider);
|
||||
|
||||
_stripeFacade.GetCustomer(providerCustomerId)
|
||||
.Returns(customer);
|
||||
|
||||
// Act
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
Assert.True(cloudRegionValid);
|
||||
|
||||
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
|
||||
await _organizationRepository.Received(1).GetByIdAsync(subscriberId);
|
||||
await _providerRepository.Received(1).GetByIdAsync(subscriberId);
|
||||
await _stripeFacade.Received(1).GetCustomer(providerCustomerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateCloudRegion_SetupIntentSucceeded_ProviderWithoutGatewayCustomerId_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
|
||||
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||
var subscriberId = Guid.NewGuid();
|
||||
|
||||
var mockProviderWithoutCustomerId = new Core.AdminConsole.Entities.Provider.Provider
|
||||
{
|
||||
Id = subscriberId,
|
||||
GatewayCustomerId = null
|
||||
};
|
||||
|
||||
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
|
||||
.Returns(subscriberId);
|
||||
|
||||
_organizationRepository.GetByIdAsync(subscriberId)
|
||||
.Returns((Core.AdminConsole.Entities.Organization?)null);
|
||||
|
||||
_providerRepository.GetByIdAsync(subscriberId)
|
||||
.Returns(mockProviderWithoutCustomerId);
|
||||
|
||||
// Act
|
||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||
|
||||
// Assert
|
||||
Assert.False(cloudRegionValid);
|
||||
|
||||
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
|
||||
await _organizationRepository.Received(1).GetByIdAsync(subscriberId);
|
||||
await _providerRepository.Received(1).GetByIdAsync(subscriberId);
|
||||
await _stripeFacade.DidNotReceive().GetCustomer(Arg.Any<string>());
|
||||
await _stripeFacade.Received(1).GetSetupIntent(
|
||||
mockSetupIntent.Id,
|
||||
Arg.Any<SetupIntentGetOptions>());
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
@@ -25,7 +24,6 @@ public class UpdatePaymentMethodCommandTests
|
||||
private readonly IBraintreeGateway _braintreeGateway = Substitute.For<IBraintreeGateway>();
|
||||
private readonly IBraintreeService _braintreeService = Substitute.For<IBraintreeService>();
|
||||
private readonly IGlobalSettings _globalSettings = Substitute.For<IGlobalSettings>();
|
||||
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
|
||||
private readonly UpdatePaymentMethodCommand _command;
|
||||
@@ -37,7 +35,6 @@ public class UpdatePaymentMethodCommandTests
|
||||
_braintreeService,
|
||||
_globalSettings,
|
||||
Substitute.For<ILogger<UpdatePaymentMethodCommand>>(),
|
||||
_setupIntentCache,
|
||||
_stripeAdapter,
|
||||
_subscriberService);
|
||||
}
|
||||
@@ -102,7 +99,8 @@ public class UpdatePaymentMethodCommandTests
|
||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
|
||||
|
||||
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
|
||||
await _stripeAdapter.Received(1).UpdateSetupIntentAsync(setupIntent.Id,
|
||||
Arg.Is<SetupIntentUpdateOptions>(options => options.Customer == customer.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -166,7 +164,8 @@ public class UpdatePaymentMethodCommandTests
|
||||
|
||||
await _subscriberService.Received(1).CreateStripeCustomer(organization);
|
||||
|
||||
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
|
||||
await _stripeAdapter.Received(1).UpdateSetupIntentAsync(setupIntent.Id,
|
||||
Arg.Is<SetupIntentUpdateOptions>(options => options.Customer == customer.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -233,7 +232,8 @@ public class UpdatePaymentMethodCommandTests
|
||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
|
||||
|
||||
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
|
||||
await _stripeAdapter.Received(1).UpdateSetupIntentAsync(setupIntent.Id,
|
||||
Arg.Is<SetupIntentUpdateOptions>(options => options.Customer == customer.Id));
|
||||
await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>
|
||||
options.Metadata[MetadataKeys.BraintreeCustomerId] == string.Empty &&
|
||||
options.Metadata[MetadataKeys.RetiredBraintreeCustomerId] == "braintree_customer_id"));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Services;
|
||||
@@ -20,7 +19,6 @@ using static StripeConstants;
|
||||
public class GetPaymentMethodQueryTests
|
||||
{
|
||||
private readonly IBraintreeService _braintreeService = Substitute.For<IBraintreeService>();
|
||||
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
|
||||
private readonly GetPaymentMethodQuery _query;
|
||||
@@ -29,7 +27,6 @@ public class GetPaymentMethodQueryTests
|
||||
{
|
||||
_query = new GetPaymentMethodQuery(
|
||||
_braintreeService,
|
||||
_setupIntentCache,
|
||||
_stripeAdapter,
|
||||
_subscriberService);
|
||||
}
|
||||
@@ -181,6 +178,7 @@ public class GetPaymentMethodQueryTests
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
@@ -189,11 +187,12 @@ public class GetPaymentMethodQueryTests
|
||||
Arg.Is<CustomerGetOptions>(options =>
|
||||
options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer);
|
||||
|
||||
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
|
||||
|
||||
_stripeAdapter
|
||||
.GetSetupIntentAsync("seti_123",
|
||||
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method"))).Returns(
|
||||
.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>
|
||||
options.Customer == customer.Id &&
|
||||
options.HasExpansions("data.payment_method")))
|
||||
.Returns(
|
||||
[
|
||||
new SetupIntent
|
||||
{
|
||||
PaymentMethod = new PaymentMethod
|
||||
@@ -209,7 +208,8 @@ public class GetPaymentMethodQueryTests
|
||||
}
|
||||
},
|
||||
Status = "requires_action"
|
||||
});
|
||||
}
|
||||
]);
|
||||
|
||||
var maskedPaymentMethod = await _query.Run(organization);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Services;
|
||||
@@ -15,7 +14,6 @@ using static StripeConstants;
|
||||
|
||||
public class HasPaymentMethodQueryTests
|
||||
{
|
||||
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
|
||||
private readonly HasPaymentMethodQuery _query;
|
||||
@@ -23,7 +21,6 @@ public class HasPaymentMethodQueryTests
|
||||
public HasPaymentMethodQueryTests()
|
||||
{
|
||||
_query = new HasPaymentMethodQuery(
|
||||
_setupIntentCache,
|
||||
_stripeAdapter,
|
||||
_subscriberService);
|
||||
}
|
||||
@@ -37,45 +34,12 @@ public class HasPaymentMethodQueryTests
|
||||
};
|
||||
|
||||
_subscriberService.GetCustomer(organization).ReturnsNull();
|
||||
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns((string)null);
|
||||
|
||||
var hasPaymentMethod = await _query.Run(organization);
|
||||
|
||||
Assert.False(hasPaymentMethod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_NoCustomer_WithUnverifiedBankAccount_ReturnsTrue()
|
||||
{
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = Guid.NewGuid()
|
||||
};
|
||||
|
||||
_subscriberService.GetCustomer(organization).ReturnsNull();
|
||||
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
|
||||
|
||||
_stripeAdapter
|
||||
.GetSetupIntentAsync("seti_123",
|
||||
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method")))
|
||||
.Returns(new SetupIntent
|
||||
{
|
||||
Status = "requires_action",
|
||||
NextAction = new SetupIntentNextAction
|
||||
{
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||
},
|
||||
PaymentMethod = new PaymentMethod
|
||||
{
|
||||
UsBankAccount = new PaymentMethodUsBankAccount()
|
||||
}
|
||||
});
|
||||
|
||||
var hasPaymentMethod = await _query.Run(organization);
|
||||
|
||||
Assert.True(hasPaymentMethod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_NoPaymentMethod_ReturnsFalse()
|
||||
{
|
||||
@@ -86,6 +50,7 @@ public class HasPaymentMethodQueryTests
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
@@ -107,6 +72,7 @@ public class HasPaymentMethodQueryTests
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
InvoiceSettings = new CustomerInvoiceSettings
|
||||
{
|
||||
DefaultPaymentMethodId = "pm_123"
|
||||
@@ -131,6 +97,7 @@ public class HasPaymentMethodQueryTests
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
DefaultSourceId = "card_123",
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
@@ -153,28 +120,32 @@ public class HasPaymentMethodQueryTests
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
_subscriberService.GetCustomer(organization).Returns(customer);
|
||||
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
|
||||
|
||||
_stripeAdapter
|
||||
.GetSetupIntentAsync("seti_123",
|
||||
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method")))
|
||||
.Returns(new SetupIntent
|
||||
{
|
||||
Status = "requires_action",
|
||||
NextAction = new SetupIntentNextAction
|
||||
.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>
|
||||
options.Customer == customer.Id &&
|
||||
options.HasExpansions("data.payment_method")))
|
||||
.Returns(
|
||||
[
|
||||
new SetupIntent
|
||||
{
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||
},
|
||||
PaymentMethod = new PaymentMethod
|
||||
{
|
||||
UsBankAccount = new PaymentMethodUsBankAccount()
|
||||
Status = "requires_action",
|
||||
NextAction = new SetupIntentNextAction
|
||||
{
|
||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||
},
|
||||
PaymentMethod = new PaymentMethod
|
||||
{
|
||||
UsBankAccount = new PaymentMethodUsBankAccount()
|
||||
}
|
||||
}
|
||||
});
|
||||
]);
|
||||
|
||||
var hasPaymentMethod = await _query.Run(organization);
|
||||
|
||||
@@ -191,6 +162,7 @@ public class HasPaymentMethodQueryTests
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
@@ -206,7 +178,7 @@ public class HasPaymentMethodQueryTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Run_NoSetupIntentId_ReturnsFalse()
|
||||
public async Task Run_NoSetupIntents_ReturnsFalse()
|
||||
{
|
||||
var organization = new Organization
|
||||
{
|
||||
@@ -215,12 +187,18 @@ public class HasPaymentMethodQueryTests
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
_subscriberService.GetCustomer(organization).Returns(customer);
|
||||
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns((string)null);
|
||||
|
||||
_stripeAdapter
|
||||
.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>
|
||||
options.Customer == customer.Id &&
|
||||
options.HasExpansions("data.payment_method")))
|
||||
.Returns(new List<SetupIntent>());
|
||||
|
||||
var hasPaymentMethod = await _query.Run(organization);
|
||||
|
||||
@@ -237,24 +215,28 @@ public class HasPaymentMethodQueryTests
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
_subscriberService.GetCustomer(organization).Returns(customer);
|
||||
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
|
||||
|
||||
_stripeAdapter
|
||||
.GetSetupIntentAsync("seti_123",
|
||||
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method")))
|
||||
.Returns(new SetupIntent
|
||||
{
|
||||
PaymentMethod = new PaymentMethod
|
||||
.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>
|
||||
options.Customer == customer.Id &&
|
||||
options.HasExpansions("data.payment_method")))
|
||||
.Returns(
|
||||
[
|
||||
new SetupIntent
|
||||
{
|
||||
Type = "card"
|
||||
},
|
||||
Status = "succeeded"
|
||||
});
|
||||
PaymentMethod = new PaymentMethod
|
||||
{
|
||||
Type = "card"
|
||||
},
|
||||
Status = "succeeded"
|
||||
}
|
||||
]);
|
||||
|
||||
var hasPaymentMethod = await _query.Run(organization);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
@@ -32,7 +31,6 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
private readonly IBraintreeGateway _braintreeGateway = Substitute.For<IBraintreeGateway>();
|
||||
private readonly IBraintreeService _braintreeService = Substitute.For<IBraintreeService>();
|
||||
private readonly IGlobalSettings _globalSettings = Substitute.For<IGlobalSettings>();
|
||||
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
|
||||
private readonly IUserService _userService = Substitute.For<IUserService>();
|
||||
@@ -63,7 +61,6 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
_braintreeGateway,
|
||||
_braintreeService,
|
||||
_globalSettings,
|
||||
_setupIntentCache,
|
||||
_stripeAdapter,
|
||||
_subscriberService,
|
||||
_userService,
|
||||
@@ -110,63 +107,6 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
Assert.Equal("Additional storage must be greater than 0.", badRequest.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_ValidPaymentMethodTypes_BankAccount_Success(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null; // Ensure no existing customer ID
|
||||
user.Email = "test@example.com";
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.BankAccount;
|
||||
paymentMethod.Token = "bank_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "active";
|
||||
mockSubscription.Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
var mockSetupIntent = Substitute.For<SetupIntent>();
|
||||
mockSetupIntent.Id = "seti_123";
|
||||
|
||||
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
_stripeAdapter.ListSetupIntentsAsync(Arg.Any<SetupIntentListOptions>()).Returns(Task.FromResult(new List<SetupIntent> { mockSetupIntent }));
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT0);
|
||||
await _stripeAdapter.Received(1).CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());
|
||||
await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());
|
||||
await _userService.Received(1).SaveUserAsync(user);
|
||||
await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_ValidPaymentMethodTypes_Card_Success(
|
||||
User user,
|
||||
@@ -625,60 +565,6 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
|
||||
await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), mockInvoice);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_BankAccountWithNoSetupIntentFound_ReturnsUnhandled(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
// Arrange
|
||||
user.Premium = false;
|
||||
user.GatewayCustomerId = null;
|
||||
user.Email = "test@example.com";
|
||||
paymentMethod.Type = TokenizablePaymentMethodType.BankAccount;
|
||||
paymentMethod.Token = "bank_token_123";
|
||||
billingAddress.Country = "US";
|
||||
billingAddress.PostalCode = "12345";
|
||||
|
||||
var mockCustomer = Substitute.For<StripeCustomer>();
|
||||
mockCustomer.Id = "cust_123";
|
||||
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
|
||||
mockCustomer.Metadata = new Dictionary<string, string>();
|
||||
|
||||
var mockSubscription = Substitute.For<StripeSubscription>();
|
||||
mockSubscription.Id = "sub_123";
|
||||
mockSubscription.Status = "incomplete";
|
||||
mockSubscription.Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var mockInvoice = Substitute.For<Invoice>();
|
||||
|
||||
_stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);
|
||||
_stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
|
||||
_stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);
|
||||
_subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);
|
||||
|
||||
_stripeAdapter.ListSetupIntentsAsync(Arg.Any<SetupIntentListOptions>())
|
||||
.Returns(Task.FromResult(new List<SetupIntent>())); // Empty list - no setup intent found
|
||||
|
||||
// Act
|
||||
var result = await _command.Run(user, paymentMethod, billingAddress, 0);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsT3);
|
||||
var unhandled = result.AsT3;
|
||||
Assert.Equal("Something went wrong with your request. Please contact support for assistance.", unhandled.Response);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_AccountCredit_WithExistingCustomer_Success(
|
||||
User user,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Braintree;
|
||||
@@ -20,7 +18,6 @@ using Xunit;
|
||||
using static Bit.Core.Test.Billing.Utilities;
|
||||
using Address = Stripe.Address;
|
||||
using Customer = Stripe.Customer;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
using PaymentMethod = Stripe.PaymentMethod;
|
||||
using Subscription = Stripe.Subscription;
|
||||
|
||||
@@ -519,10 +516,11 @@ public class SubscriberServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetSetupIntentAsync(setupIntent.Id,
|
||||
Arg.Is<SetupIntentGetOptions>(options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
|
||||
sutProvider.GetDependency<IStripeAdapter>().ListSetupIntentsAsync(
|
||||
Arg.Is<SetupIntentListOptions>(options =>
|
||||
options.Customer == customer.Id &&
|
||||
options.Expand.Contains("data.payment_method")))
|
||||
.Returns([setupIntent]);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
|
||||
@@ -1032,423 +1030,6 @@ public class SubscriberServiceTests
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UpdatePaymentMethod
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_NullSubscriber_ThrowsArgumentNullException(
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
=> await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdatePaymentSource(null, null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_NullTokenizedPaymentMethod_ThrowsArgumentNullException(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
=> await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdatePaymentSource(provider, null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_NoToken_ThrowsBillingException(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
|
||||
.Returns(new Customer());
|
||||
|
||||
await ThrowsBillingExceptionAsync(() =>
|
||||
sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.Card, null)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_UnsupportedPaymentMethod_ThrowsBillingException(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
|
||||
.Returns(new Customer());
|
||||
|
||||
await ThrowsBillingExceptionAsync(() =>
|
||||
sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.BitPay, "TOKEN")));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_BankAccount_IncorrectNumberOfSetupIntentsForToken_ThrowsBillingException(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId)
|
||||
.Returns(new Customer());
|
||||
|
||||
stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options => options.PaymentMethod == "TOKEN"))
|
||||
.Returns([new SetupIntent(), new SetupIntent()]);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() =>
|
||||
sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.BankAccount, "TOKEN")));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_BankAccount_Succeeds(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
stripeAdapter.GetCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = provider.GatewayCustomerId,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[Core.Billing.Utilities.BraintreeCustomerIdKey] = "braintree_customer_id"
|
||||
}
|
||||
});
|
||||
|
||||
var matchingSetupIntent = new SetupIntent { Id = "setup_intent_1" };
|
||||
|
||||
stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options => options.PaymentMethod == "TOKEN"))
|
||||
.Returns([matchingSetupIntent]);
|
||||
|
||||
stripeAdapter.ListCustomerPaymentMethodsAsync(provider.GatewayCustomerId).Returns([
|
||||
new PaymentMethod { Id = "payment_method_1" }
|
||||
]);
|
||||
|
||||
await sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.BankAccount, "TOKEN"));
|
||||
|
||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_1");
|
||||
|
||||
await stripeAdapter.DidNotReceive().CancelSetupIntentAsync(Arg.Any<string>(),
|
||||
Arg.Any<SetupIntentCancelOptions>());
|
||||
|
||||
await stripeAdapter.Received(1).DetachPaymentMethodAsync("payment_method_1");
|
||||
|
||||
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
||||
options => options.Metadata[Core.Billing.Utilities.BraintreeCustomerIdKey] == null));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_Card_Succeeds(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
stripeAdapter.GetCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))
|
||||
)
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = provider.GatewayCustomerId,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[Core.Billing.Utilities.BraintreeCustomerIdKey] = "braintree_customer_id"
|
||||
}
|
||||
});
|
||||
|
||||
stripeAdapter.ListCustomerPaymentMethodsAsync(provider.GatewayCustomerId).Returns([
|
||||
new PaymentMethod { Id = "payment_method_1" }
|
||||
]);
|
||||
|
||||
await sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.Card, "TOKEN"));
|
||||
|
||||
await stripeAdapter.DidNotReceive().CancelSetupIntentAsync(Arg.Any<string>(),
|
||||
Arg.Any<SetupIntentCancelOptions>());
|
||||
|
||||
await stripeAdapter.Received(1).DetachPaymentMethodAsync("payment_method_1");
|
||||
|
||||
await stripeAdapter.Received(1).AttachPaymentMethodAsync("TOKEN", Arg.Is<PaymentMethodAttachOptions>(
|
||||
options => options.Customer == provider.GatewayCustomerId));
|
||||
|
||||
await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
||||
options =>
|
||||
options.InvoiceSettings.DefaultPaymentMethod == "TOKEN" &&
|
||||
options.Metadata[Core.Billing.Utilities.BraintreeCustomerIdKey] == null));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_Braintree_NullCustomer_ThrowsBillingException(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
const string braintreeCustomerId = "braintree_customer_id";
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = provider.GatewayCustomerId,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId
|
||||
}
|
||||
});
|
||||
|
||||
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
|
||||
|
||||
customerGateway.FindAsync(braintreeCustomerId).ReturnsNull();
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
|
||||
|
||||
await paymentMethodGateway.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<PaymentMethodRequest>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_Braintree_ReplacePaymentMethod_CreatePaymentMethodFails_ThrowsBillingException(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
const string braintreeCustomerId = "braintree_customer_id";
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = provider.GatewayCustomerId,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId
|
||||
}
|
||||
});
|
||||
|
||||
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
|
||||
|
||||
var customer = Substitute.For<Braintree.Customer>();
|
||||
|
||||
customer.Id.Returns(braintreeCustomerId);
|
||||
|
||||
customerGateway.FindAsync(braintreeCustomerId).Returns(customer);
|
||||
|
||||
var createPaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();
|
||||
|
||||
createPaymentMethodResult.IsSuccess().Returns(false);
|
||||
|
||||
paymentMethodGateway.CreateAsync(Arg.Is<PaymentMethodRequest>(
|
||||
options => options.CustomerId == braintreeCustomerId && options.PaymentMethodNonce == "TOKEN"))
|
||||
.Returns(createPaymentMethodResult);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
|
||||
|
||||
await customerGateway.DidNotReceiveWithAnyArgs().UpdateAsync(Arg.Any<string>(), Arg.Any<CustomerRequest>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_Braintree_ReplacePaymentMethod_UpdateCustomerFails_DeletePaymentMethod_ThrowsBillingException(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
const string braintreeCustomerId = "braintree_customer_id";
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = provider.GatewayCustomerId,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId
|
||||
}
|
||||
});
|
||||
|
||||
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
|
||||
|
||||
var customer = Substitute.For<Braintree.Customer>();
|
||||
|
||||
customer.Id.Returns(braintreeCustomerId);
|
||||
|
||||
customerGateway.FindAsync(braintreeCustomerId).Returns(customer);
|
||||
|
||||
var createPaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();
|
||||
|
||||
var createdPaymentMethod = Substitute.For<Braintree.PaymentMethod>();
|
||||
|
||||
createdPaymentMethod.Token.Returns("TOKEN");
|
||||
|
||||
createPaymentMethodResult.IsSuccess().Returns(true);
|
||||
|
||||
createPaymentMethodResult.Target.Returns(createdPaymentMethod);
|
||||
|
||||
paymentMethodGateway.CreateAsync(Arg.Is<PaymentMethodRequest>(
|
||||
options => options.CustomerId == braintreeCustomerId && options.PaymentMethodNonce == "TOKEN"))
|
||||
.Returns(createPaymentMethodResult);
|
||||
|
||||
var updateCustomerResult = Substitute.For<Result<Braintree.Customer>>();
|
||||
|
||||
updateCustomerResult.IsSuccess().Returns(false);
|
||||
|
||||
customerGateway.UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(options =>
|
||||
options.DefaultPaymentMethodToken == createPaymentMethodResult.Target.Token))
|
||||
.Returns(updateCustomerResult);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
|
||||
|
||||
await paymentMethodGateway.Received(1).DeleteAsync(createPaymentMethodResult.Target.Token);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_Braintree_ReplacePaymentMethod_Success(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
const string braintreeCustomerId = "braintree_customer_id";
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = provider.GatewayCustomerId,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId
|
||||
}
|
||||
});
|
||||
|
||||
var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
|
||||
|
||||
var customer = Substitute.For<Braintree.Customer>();
|
||||
|
||||
var existingPaymentMethod = Substitute.For<Braintree.PaymentMethod>();
|
||||
|
||||
existingPaymentMethod.Token.Returns("OLD_TOKEN");
|
||||
|
||||
existingPaymentMethod.IsDefault.Returns(true);
|
||||
|
||||
customer.PaymentMethods.Returns([existingPaymentMethod]);
|
||||
|
||||
customer.Id.Returns(braintreeCustomerId);
|
||||
|
||||
customerGateway.FindAsync(braintreeCustomerId).Returns(customer);
|
||||
|
||||
var createPaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();
|
||||
|
||||
var updatedPaymentMethod = Substitute.For<Braintree.PaymentMethod>();
|
||||
|
||||
updatedPaymentMethod.Token.Returns("TOKEN");
|
||||
|
||||
createPaymentMethodResult.IsSuccess().Returns(true);
|
||||
|
||||
createPaymentMethodResult.Target.Returns(updatedPaymentMethod);
|
||||
|
||||
paymentMethodGateway.CreateAsync(Arg.Is<PaymentMethodRequest>(
|
||||
options => options.CustomerId == braintreeCustomerId && options.PaymentMethodNonce == "TOKEN"))
|
||||
.Returns(createPaymentMethodResult);
|
||||
|
||||
var updateCustomerResult = Substitute.For<Result<Braintree.Customer>>();
|
||||
|
||||
updateCustomerResult.IsSuccess().Returns(true);
|
||||
|
||||
customerGateway.UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(options =>
|
||||
options.DefaultPaymentMethodToken == createPaymentMethodResult.Target.Token))
|
||||
.Returns(updateCustomerResult);
|
||||
|
||||
var deletePaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();
|
||||
|
||||
deletePaymentMethodResult.IsSuccess().Returns(true);
|
||||
|
||||
paymentMethodGateway.DeleteAsync(existingPaymentMethod.Token).Returns(deletePaymentMethodResult);
|
||||
|
||||
await sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN"));
|
||||
|
||||
await paymentMethodGateway.Received(1).DeleteAsync(existingPaymentMethod.Token);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_Braintree_CreateCustomer_CustomerUpdateFails_ThrowsBillingException(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
const string braintreeCustomerId = "braintree_customer_id";
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId)
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = provider.GatewayCustomerId
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
|
||||
.Returns(new GlobalSettings.BaseServiceUriSettings(new GlobalSettings())
|
||||
{
|
||||
CloudRegion = "US"
|
||||
});
|
||||
|
||||
var (_, customerGateway, _) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
|
||||
|
||||
var createCustomerResult = Substitute.For<Result<Braintree.Customer>>();
|
||||
|
||||
createCustomerResult.IsSuccess().Returns(false);
|
||||
|
||||
customerGateway.CreateAsync(Arg.Is<CustomerRequest>(
|
||||
options =>
|
||||
options.Id == braintreeCustomerId &&
|
||||
options.CustomFields[provider.BraintreeIdField()] == provider.Id.ToString() &&
|
||||
options.CustomFields[provider.BraintreeCloudRegionField()] == "US" &&
|
||||
options.Email == provider.BillingEmailAddress() &&
|
||||
options.PaymentMethodNonce == "TOKEN"))
|
||||
.Returns(createCustomerResult);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() =>
|
||||
sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
|
||||
.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_Braintree_CreateCustomer_Succeeds(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
{
|
||||
const string braintreeCustomerId = "braintree_customer_id";
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(
|
||||
provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids")))
|
||||
.Returns(new Customer
|
||||
{
|
||||
Id = provider.GatewayCustomerId
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
|
||||
.Returns(new GlobalSettings.BaseServiceUriSettings(new GlobalSettings())
|
||||
{
|
||||
CloudRegion = "US"
|
||||
});
|
||||
|
||||
var (_, customerGateway, _) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());
|
||||
|
||||
var createCustomerResult = Substitute.For<Result<Braintree.Customer>>();
|
||||
|
||||
var createdCustomer = Substitute.For<Braintree.Customer>();
|
||||
|
||||
createdCustomer.Id.Returns(braintreeCustomerId);
|
||||
|
||||
createCustomerResult.IsSuccess().Returns(true);
|
||||
|
||||
createCustomerResult.Target.Returns(createdCustomer);
|
||||
|
||||
customerGateway.CreateAsync(Arg.Is<CustomerRequest>(
|
||||
options =>
|
||||
options.CustomFields[provider.BraintreeIdField()] == provider.Id.ToString() &&
|
||||
options.CustomFields[provider.BraintreeCloudRegionField()] == "US" &&
|
||||
options.Email == provider.BillingEmailAddress() &&
|
||||
options.PaymentMethodNonce == "TOKEN"))
|
||||
.Returns(createCustomerResult);
|
||||
|
||||
await sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN"));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateCustomerAsync(provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(
|
||||
options => options.Metadata[Core.Billing.Utilities.BraintreeCustomerIdKey] == braintreeCustomerId));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateTaxInformation
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
Reference in New Issue
Block a user