mirror of
https://github.com/bitwarden/server
synced 2026-02-19 10:53:34 +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:
@@ -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