mirror of
https://github.com/bitwarden/server.git
synced 2025-03-12 13:29:14 +01:00
[BEEEP] [PM-18518] Cleanup StripePaymentService (#5435)
This commit is contained in:
parent
6cb97d9bf9
commit
c589f9a330
@ -22,4 +22,9 @@ public static class CustomerExtensions
|
||||
/// <returns></returns>
|
||||
public static bool HasTaxLocationVerified(this Customer customer) =>
|
||||
customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
|
||||
|
||||
public static decimal GetBillingBalance(this Customer customer)
|
||||
{
|
||||
return customer != null ? customer.Balance / 100M : default;
|
||||
}
|
||||
}
|
||||
|
26
src/Core/Billing/Extensions/SubscriberExtensions.cs
Normal file
26
src/Core/Billing/Extensions/SubscriberExtensions.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Billing.Extensions;
|
||||
|
||||
public static class SubscriberExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// We are taking only first 30 characters of the SubscriberName because stripe provide for 30 characters for
|
||||
/// custom_fields,see the link: https://stripe.com/docs/api/invoices/create
|
||||
/// </summary>
|
||||
/// <param name="subscriber"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetFormattedInvoiceName(this ISubscriber subscriber)
|
||||
{
|
||||
var subscriberName = subscriber.SubscriberName();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subscriberName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return subscriberName.Length <= 30
|
||||
? subscriberName
|
||||
: subscriberName[..30];
|
||||
}
|
||||
}
|
@ -14,18 +14,8 @@ namespace Bit.Core.Services;
|
||||
public interface IPaymentService
|
||||
{
|
||||
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
||||
Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
|
||||
bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0,
|
||||
int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false);
|
||||
Task<string> PurchaseOrganizationNoPaymentMethod(Organization org, Plan plan, int additionalSeats,
|
||||
bool premiumAccessAddon, int additionalSmSeats = 0, int additionalServiceAccount = 0,
|
||||
bool signupIsFromSecretsManagerTrial = false);
|
||||
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
|
||||
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
||||
short additionalStorageGb, TaxInfo taxInfo);
|
||||
Task<string> AdjustSubscription(
|
||||
Organization organization,
|
||||
Plan updatedPlan,
|
||||
@ -56,9 +46,7 @@ public interface IPaymentService
|
||||
Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo);
|
||||
Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats,
|
||||
int additionalServiceAccount);
|
||||
Task<bool> RisksSubscriptionFailure(Organization organization);
|
||||
Task<bool> HasSecretsManagerStandalone(Organization organization);
|
||||
Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription);
|
||||
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
||||
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
||||
|
||||
|
@ -25,9 +25,6 @@ namespace Bit.Core.Services;
|
||||
|
||||
public class StripePaymentService : IPaymentService
|
||||
{
|
||||
private const string PremiumPlanId = "premium-annually";
|
||||
private const string StoragePlanId = "storage-gb-annually";
|
||||
private const string ProviderDiscountId = "msp-discount-35";
|
||||
private const string SecretsManagerStandaloneDiscountId = "sm-standalone";
|
||||
|
||||
private readonly ITransactionRepository _transactionRepository;
|
||||
@ -62,240 +59,6 @@ public class StripePaymentService : IPaymentService
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||
string paymentToken, StaticStore.Plan plan, short additionalStorageGb,
|
||||
int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false,
|
||||
int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false)
|
||||
{
|
||||
Braintree.Customer braintreeCustomer = null;
|
||||
string stipeCustomerSourceToken = null;
|
||||
string stipeCustomerPaymentMethodId = null;
|
||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
||||
};
|
||||
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
|
||||
paymentMethodType == PaymentMethodType.BankAccount;
|
||||
|
||||
if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
if (paymentToken.StartsWith("pm_"))
|
||||
{
|
||||
stipeCustomerPaymentMethodId = paymentToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
stipeCustomerSourceToken = paymentToken;
|
||||
}
|
||||
}
|
||||
else if (paymentMethodType == PaymentMethodType.PayPal)
|
||||
{
|
||||
var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
||||
var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest
|
||||
{
|
||||
PaymentMethodNonce = paymentToken,
|
||||
Email = org.BillingEmail,
|
||||
Id = org.BraintreeCustomerIdPrefix() + org.Id.ToString("N").ToLower() + randomSuffix,
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[org.BraintreeIdField()] = org.Id.ToString(),
|
||||
[org.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion
|
||||
}
|
||||
});
|
||||
|
||||
if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
|
||||
{
|
||||
throw new GatewayException("Failed to create PayPal customer record.");
|
||||
}
|
||||
|
||||
braintreeCustomer = customerResult.Target;
|
||||
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new GatewayException("Payment method is not supported at this time.");
|
||||
}
|
||||
|
||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
|
||||
, additionalSmSeats, additionalServiceAccount);
|
||||
|
||||
Customer customer = null;
|
||||
Subscription subscription;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber))
|
||||
{
|
||||
taxInfo.TaxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry,
|
||||
taxInfo.TaxIdNumber);
|
||||
|
||||
if (taxInfo.TaxIdType == null)
|
||||
{
|
||||
_logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
||||
taxInfo.BillingAddressCountry,
|
||||
taxInfo.TaxIdNumber);
|
||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||
}
|
||||
}
|
||||
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Description = org.DisplayBusinessName(),
|
||||
Email = org.BillingEmail,
|
||||
Source = stipeCustomerSourceToken,
|
||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||
Metadata = stripeCustomerMetadata,
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = org.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
||||
}
|
||||
],
|
||||
},
|
||||
Coupon = signupIsFromSecretsManagerTrial
|
||||
? SecretsManagerStandaloneDiscountId
|
||||
: provider
|
||||
? ProviderDiscountId
|
||||
: null,
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Country = taxInfo?.BillingAddressCountry,
|
||||
PostalCode = taxInfo?.BillingAddressPostalCode,
|
||||
// Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead.
|
||||
Line1 = taxInfo?.BillingAddressLine1 ?? string.Empty,
|
||||
Line2 = taxInfo?.BillingAddressLine2,
|
||||
City = taxInfo?.BillingAddressCity,
|
||||
State = taxInfo?.BillingAddressState,
|
||||
},
|
||||
TaxIdData = !string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)
|
||||
? [new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber }]
|
||||
: null
|
||||
};
|
||||
|
||||
customerCreateOptions.AddExpand("tax");
|
||||
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||
subCreateOptions.Customer = customer.Id;
|
||||
subCreateOptions.EnableAutomaticTax(customer);
|
||||
|
||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
||||
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
|
||||
{
|
||||
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
|
||||
{
|
||||
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions());
|
||||
throw new GatewayException("Payment method was declined.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating customer, walking back operation.");
|
||||
if (customer != null)
|
||||
{
|
||||
await _stripeAdapter.CustomerDeleteAsync(customer.Id);
|
||||
}
|
||||
if (braintreeCustomer != null)
|
||||
{
|
||||
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
org.Gateway = GatewayType.Stripe;
|
||||
org.GatewayCustomerId = customer.Id;
|
||||
org.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
if (subscription.Status == "incomplete" &&
|
||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
||||
{
|
||||
org.Enabled = false;
|
||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||
}
|
||||
else
|
||||
{
|
||||
org.Enabled = true;
|
||||
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> PurchaseOrganizationNoPaymentMethod(Organization org, StaticStore.Plan plan, int additionalSeats, bool premiumAccessAddon,
|
||||
int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false)
|
||||
{
|
||||
|
||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
||||
};
|
||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, new TaxInfo(), additionalSeats, 0, premiumAccessAddon
|
||||
, additionalSmSeats, additionalServiceAccount);
|
||||
|
||||
Customer customer = null;
|
||||
Subscription subscription;
|
||||
try
|
||||
{
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Description = org.DisplayBusinessName(),
|
||||
Email = org.BillingEmail,
|
||||
Metadata = stripeCustomerMetadata,
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = org.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
||||
}
|
||||
],
|
||||
},
|
||||
Coupon = signupIsFromSecretsManagerTrial
|
||||
? SecretsManagerStandaloneDiscountId
|
||||
: null,
|
||||
TaxIdData = null,
|
||||
};
|
||||
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||
subCreateOptions.Customer = customer.Id;
|
||||
|
||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating customer, walking back operation.");
|
||||
if (customer != null)
|
||||
{
|
||||
await _stripeAdapter.CustomerDeleteAsync(customer.Id);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
org.Gateway = GatewayType.Stripe;
|
||||
org.GatewayCustomerId = customer.Id;
|
||||
org.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
if (subscription.Status == "incomplete" &&
|
||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
||||
{
|
||||
org.Enabled = false;
|
||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||
}
|
||||
|
||||
org.Enabled = true;
|
||||
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
private async Task ChangeOrganizationSponsorship(
|
||||
Organization org,
|
||||
OrganizationSponsorship sponsorship,
|
||||
@ -324,458 +87,6 @@ public class StripePaymentService : IPaymentService
|
||||
public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) =>
|
||||
ChangeOrganizationSponsorship(org, sponsorship, false);
|
||||
|
||||
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan,
|
||||
OrganizationUpgrade upgrade)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
|
||||
{
|
||||
throw new BadRequestException("Organization already has a subscription.");
|
||||
}
|
||||
|
||||
var customerOptions = new CustomerGetOptions();
|
||||
customerOptions.AddExpand("default_source");
|
||||
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
||||
customerOptions.AddExpand("tax");
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions);
|
||||
if (customer == null)
|
||||
{
|
||||
throw new GatewayException("Could not find customer payment profile.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(upgrade.TaxInfo?.BillingAddressCountry) &&
|
||||
!string.IsNullOrEmpty(upgrade.TaxInfo?.BillingAddressPostalCode))
|
||||
{
|
||||
var addressOptions = new AddressOptions
|
||||
{
|
||||
Country = upgrade.TaxInfo.BillingAddressCountry,
|
||||
PostalCode = upgrade.TaxInfo.BillingAddressPostalCode,
|
||||
// Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead.
|
||||
Line1 = upgrade.TaxInfo.BillingAddressLine1 ?? string.Empty,
|
||||
Line2 = upgrade.TaxInfo.BillingAddressLine2,
|
||||
City = upgrade.TaxInfo.BillingAddressCity,
|
||||
State = upgrade.TaxInfo.BillingAddressState,
|
||||
};
|
||||
var customerUpdateOptions = new CustomerUpdateOptions { Address = addressOptions };
|
||||
customerUpdateOptions.AddExpand("default_source");
|
||||
customerUpdateOptions.AddExpand("invoice_settings.default_payment_method");
|
||||
customerUpdateOptions.AddExpand("tax");
|
||||
customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions);
|
||||
}
|
||||
|
||||
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade);
|
||||
|
||||
subCreateOptions.EnableAutomaticTax(customer);
|
||||
|
||||
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
|
||||
|
||||
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
|
||||
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
|
||||
org.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
if (subscription.Status == "incomplete" &&
|
||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
||||
{
|
||||
org.Enabled = false;
|
||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||
}
|
||||
else
|
||||
{
|
||||
org.Enabled = true;
|
||||
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
|
||||
Customer customer, SubscriptionCreateOptions subCreateOptions)
|
||||
{
|
||||
var stripePaymentMethod = false;
|
||||
var paymentMethodType = PaymentMethodType.Credit;
|
||||
var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId");
|
||||
if (hasBtCustomerId)
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.PayPal;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card")
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.Card;
|
||||
stripePaymentMethod = true;
|
||||
}
|
||||
else if (customer.DefaultSource != null)
|
||||
{
|
||||
if (customer.DefaultSource is Card || customer.DefaultSource is SourceCard)
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.Card;
|
||||
stripePaymentMethod = true;
|
||||
}
|
||||
else if (customer.DefaultSource is BankAccount || customer.DefaultSource is SourceAchDebit)
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.BankAccount;
|
||||
stripePaymentMethod = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var paymentMethod = GetLatestCardPaymentMethod(customer.Id);
|
||||
if (paymentMethod != null)
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.Card;
|
||||
stripePaymentMethod = true;
|
||||
subCreateOptions.DefaultPaymentMethod = paymentMethod.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (stripePaymentMethod, paymentMethodType);
|
||||
}
|
||||
|
||||
public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType,
|
||||
string paymentToken, short additionalStorageGb, TaxInfo taxInfo)
|
||||
{
|
||||
if (paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
throw new BadRequestException("Payment token is required.");
|
||||
}
|
||||
if (paymentMethodType == PaymentMethodType.Credit &&
|
||||
(user.Gateway != GatewayType.Stripe || string.IsNullOrWhiteSpace(user.GatewayCustomerId)))
|
||||
{
|
||||
throw new BadRequestException("Your account does not have any credit available.");
|
||||
}
|
||||
if (paymentMethodType is PaymentMethodType.BankAccount)
|
||||
{
|
||||
throw new GatewayException("Payment method is not supported at this time.");
|
||||
}
|
||||
|
||||
var createdStripeCustomer = false;
|
||||
Customer customer = null;
|
||||
Braintree.Customer braintreeCustomer = null;
|
||||
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount
|
||||
or PaymentMethodType.Credit;
|
||||
|
||||
string stipeCustomerPaymentMethodId = null;
|
||||
string stipeCustomerSourceToken = null;
|
||||
if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
if (paymentToken.StartsWith("pm_"))
|
||||
{
|
||||
stipeCustomerPaymentMethodId = paymentToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
stipeCustomerSourceToken = paymentToken;
|
||||
}
|
||||
}
|
||||
|
||||
if (user.Gateway == GatewayType.Stripe && !string.IsNullOrWhiteSpace(user.GatewayCustomerId))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var customerGetOptions = new CustomerGetOptions();
|
||||
customerGetOptions.AddExpand("tax");
|
||||
customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId, customerGetOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Attempted to get existing customer from Stripe, but customer ID was not found. Attempting to recreate customer...");
|
||||
}
|
||||
}
|
||||
|
||||
if (customer == null && !string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
||||
};
|
||||
if (paymentMethodType == PaymentMethodType.PayPal)
|
||||
{
|
||||
var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
||||
var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest
|
||||
{
|
||||
PaymentMethodNonce = paymentToken,
|
||||
Email = user.Email,
|
||||
Id = user.BraintreeCustomerIdPrefix() + user.Id.ToString("N").ToLower() + randomSuffix,
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[user.BraintreeIdField()] = user.Id.ToString(),
|
||||
[user.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion
|
||||
}
|
||||
});
|
||||
|
||||
if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
|
||||
{
|
||||
throw new GatewayException("Failed to create PayPal customer record.");
|
||||
}
|
||||
|
||||
braintreeCustomer = customerResult.Target;
|
||||
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
|
||||
}
|
||||
else if (!stripePaymentMethod)
|
||||
{
|
||||
throw new GatewayException("Payment method is not supported at this time.");
|
||||
}
|
||||
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Description = user.Name,
|
||||
Email = user.Email,
|
||||
Metadata = stripeCustomerMetadata,
|
||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||
Source = stipeCustomerSourceToken,
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = user.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(user.SubscriberName()),
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Line1 = string.Empty,
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
},
|
||||
};
|
||||
customerCreateOptions.AddExpand("tax");
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
createdStripeCustomer = true;
|
||||
}
|
||||
|
||||
if (customer == null)
|
||||
{
|
||||
throw new GatewayException("Could not set up customer payment profile.");
|
||||
}
|
||||
|
||||
var subCreateOptions = new SubscriptionCreateOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
Items = [],
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[user.GatewayIdField()] = user.Id.ToString()
|
||||
}
|
||||
};
|
||||
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = PremiumPlanId,
|
||||
Quantity = 1
|
||||
});
|
||||
|
||||
if (additionalStorageGb > 0)
|
||||
{
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = StoragePlanId,
|
||||
Quantity = additionalStorageGb
|
||||
});
|
||||
}
|
||||
|
||||
subCreateOptions.EnableAutomaticTax(customer);
|
||||
|
||||
var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer,
|
||||
stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer);
|
||||
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
user.GatewayCustomerId = customer.Id;
|
||||
user.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
if (subscription.Status == "incomplete" &&
|
||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
||||
{
|
||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||
}
|
||||
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<Subscription> ChargeForNewSubscriptionAsync(ISubscriber subscriber, Customer customer,
|
||||
bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType,
|
||||
SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
|
||||
{
|
||||
var addedCreditToStripeCustomer = false;
|
||||
Braintree.Transaction braintreeTransaction = null;
|
||||
|
||||
var subInvoiceMetadata = new Dictionary<string, string>();
|
||||
Subscription subscription = null;
|
||||
try
|
||||
{
|
||||
if (!stripePaymentMethod)
|
||||
{
|
||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items)
|
||||
});
|
||||
|
||||
if (customer.HasTaxLocationVerified())
|
||||
{
|
||||
previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true };
|
||||
}
|
||||
|
||||
if (previewInvoice.AmountDue > 0)
|
||||
{
|
||||
var braintreeCustomerId = customer.Metadata != null &&
|
||||
customer.Metadata.ContainsKey("btCustomerId") ? customer.Metadata["btCustomerId"] : null;
|
||||
if (!string.IsNullOrWhiteSpace(braintreeCustomerId))
|
||||
{
|
||||
var btInvoiceAmount = (previewInvoice.AmountDue / 100M);
|
||||
var transactionResult = await _btGateway.Transaction.SaleAsync(
|
||||
new Braintree.TransactionRequest
|
||||
{
|
||||
Amount = btInvoiceAmount,
|
||||
CustomerId = braintreeCustomerId,
|
||||
Options = new Braintree.TransactionOptionsRequest
|
||||
{
|
||||
SubmitForSettlement = true,
|
||||
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
||||
{
|
||||
CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id},{subscriber.BraintreeCloudRegionField()}:{_globalSettings.BaseServiceUri.CloudRegion}"
|
||||
}
|
||||
},
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[subscriber.BraintreeIdField()] = subscriber.Id.ToString(),
|
||||
[subscriber.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion
|
||||
}
|
||||
});
|
||||
|
||||
if (!transactionResult.IsSuccess())
|
||||
{
|
||||
throw new GatewayException("Failed to charge PayPal customer.");
|
||||
}
|
||||
|
||||
braintreeTransaction = transactionResult.Target;
|
||||
subInvoiceMetadata.Add("btTransactionId", braintreeTransaction.Id);
|
||||
subInvoiceMetadata.Add("btPayPalTransactionId",
|
||||
braintreeTransaction.PayPalDetails.AuthorizationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new GatewayException("No payment was able to be collected.");
|
||||
}
|
||||
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Balance = customer.Balance - previewInvoice.AmountDue
|
||||
});
|
||||
addedCreditToStripeCustomer = true;
|
||||
}
|
||||
}
|
||||
else if (paymentMethodType == PaymentMethodType.Credit)
|
||||
{
|
||||
var upcomingInvoiceOptions = new UpcomingInvoiceOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items),
|
||||
SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates,
|
||||
};
|
||||
|
||||
upcomingInvoiceOptions.EnableAutomaticTax(customer, null);
|
||||
|
||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions);
|
||||
|
||||
if (previewInvoice.AmountDue > 0)
|
||||
{
|
||||
throw new GatewayException("Your account does not have enough credit available.");
|
||||
}
|
||||
}
|
||||
|
||||
subCreateOptions.OffSession = true;
|
||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||
|
||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
||||
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
|
||||
{
|
||||
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
|
||||
{
|
||||
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions());
|
||||
throw new GatewayException("Payment method was declined.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!stripePaymentMethod && subInvoiceMetadata.Any())
|
||||
{
|
||||
var invoices = await _stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions
|
||||
{
|
||||
Subscription = subscription.Id
|
||||
});
|
||||
|
||||
var invoice = invoices?.FirstOrDefault();
|
||||
if (invoice == null)
|
||||
{
|
||||
throw new GatewayException("Invoice not found.");
|
||||
}
|
||||
|
||||
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions
|
||||
{
|
||||
Metadata = subInvoiceMetadata
|
||||
});
|
||||
}
|
||||
|
||||
return subscription;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (customer != null)
|
||||
{
|
||||
if (createdStripeCustomer)
|
||||
{
|
||||
await _stripeAdapter.CustomerDeleteAsync(customer.Id);
|
||||
}
|
||||
else if (addedCreditToStripeCustomer || customer.Balance < 0)
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Balance = customer.Balance
|
||||
});
|
||||
}
|
||||
}
|
||||
if (braintreeTransaction != null)
|
||||
{
|
||||
await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id);
|
||||
}
|
||||
if (braintreeCustomer != null)
|
||||
{
|
||||
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
||||
}
|
||||
|
||||
if (e is StripeException strEx &&
|
||||
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
||||
{
|
||||
throw new GatewayException("Bank account is not yet verified.");
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
||||
List<SubscriptionItemOptions> subItemOptions)
|
||||
{
|
||||
return subItemOptions.Select(si => new InvoiceSubscriptionItemOptions
|
||||
{
|
||||
Plan = si.Plan,
|
||||
Price = si.Price,
|
||||
Quantity = si.Quantity,
|
||||
Id = si.Id
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task<string> FinalizeSubscriptionChangeAsync(ISubscriber subscriber,
|
||||
SubscriptionUpdate subscriptionUpdate, bool invoiceNow = false)
|
||||
{
|
||||
@ -1400,7 +711,7 @@ public class StripePaymentService : IPaymentService
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = subscriber.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()),
|
||||
Value = subscriber.GetFormattedInvoiceName()
|
||||
}
|
||||
|
||||
]
|
||||
@ -1492,7 +803,7 @@ public class StripePaymentService : IPaymentService
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = subscriber.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName())
|
||||
Value = subscriber.GetFormattedInvoiceName()
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -1560,7 +871,7 @@ public class StripePaymentService : IPaymentService
|
||||
var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions());
|
||||
var billingInfo = new BillingInfo
|
||||
{
|
||||
Balance = GetBillingBalance(customer),
|
||||
Balance = customer.GetBillingBalance(),
|
||||
PaymentSource = await GetBillingPaymentSourceAsync(customer)
|
||||
};
|
||||
|
||||
@ -1768,27 +1079,6 @@ public class StripePaymentService : IPaymentService
|
||||
new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount),
|
||||
true);
|
||||
|
||||
public async Task<bool> RisksSubscriptionFailure(Organization organization)
|
||||
{
|
||||
var subscriptionInfo = await GetSubscriptionAsync(organization);
|
||||
|
||||
if (subscriptionInfo.Subscription is not
|
||||
{
|
||||
Status: "active" or "trialing" or "past_due",
|
||||
CollectionMethod: "charge_automatically"
|
||||
}
|
||||
|| subscriptionInfo.UpcomingInvoice == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var customer = await GetCustomerAsync(organization.GatewayCustomerId, GetCustomerPaymentOptions());
|
||||
|
||||
var paymentSource = await GetBillingPaymentSourceAsync(customer);
|
||||
|
||||
return paymentSource == null;
|
||||
}
|
||||
|
||||
public async Task<bool> HasSecretsManagerStandalone(Organization organization)
|
||||
{
|
||||
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||
@ -1801,7 +1091,7 @@ public class StripePaymentService : IPaymentService
|
||||
return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId;
|
||||
}
|
||||
|
||||
public async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription)
|
||||
private async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription)
|
||||
{
|
||||
if (subscription.Status is not "past_due" && subscription.Status is not "unpaid")
|
||||
{
|
||||
@ -2117,11 +1407,6 @@ public class StripePaymentService : IPaymentService
|
||||
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
|
||||
}
|
||||
|
||||
private decimal GetBillingBalance(Customer customer)
|
||||
{
|
||||
return customer != null ? customer.Balance / 100M : default;
|
||||
}
|
||||
|
||||
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Customer customer)
|
||||
{
|
||||
if (customer == null)
|
||||
@ -2252,18 +1537,4 @@ public class StripePaymentService : IPaymentService
|
||||
throw new GatewayException("Failed to retrieve current invoices", exception);
|
||||
}
|
||||
}
|
||||
|
||||
// We are taking only first 30 characters of the SubscriberName because stripe provide
|
||||
// for 30 characters for custom_fields,see the link: https://stripe.com/docs/api/invoices/create
|
||||
private static string GetFirstThirtyCharacters(string subscriberName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(subscriberName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return subscriberName.Length <= 30
|
||||
? subscriberName
|
||||
: subscriberName[..30];
|
||||
}
|
||||
}
|
||||
|
23
test/Core.Test/Extensions/SubscriberExtensionsTests.cs
Normal file
23
test/Core.Test/Extensions/SubscriberExtensionsTests.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Extensions;
|
||||
|
||||
public class SubscriberExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Alexandria Villanueva Gonzalez Pablo", "Alexandria Villanueva Gonzalez")]
|
||||
[InlineData("John Snow", "John Snow")]
|
||||
public void GetFormattedInvoiceName_Returns_FirstThirtyCaractersOfName(string name, string expected)
|
||||
{
|
||||
// arrange
|
||||
var provider = new Provider { Name = name };
|
||||
|
||||
// act
|
||||
var actual = provider.GetFormattedInvoiceName();
|
||||
|
||||
// assert
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
@ -1,828 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Braintree;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Customer = Braintree.Customer;
|
||||
using PaymentMethod = Braintree.PaymentMethod;
|
||||
using PaymentMethodType = Bit.Core.Enums.PaymentMethodType;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class StripePaymentServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData(PaymentMethodType.BitPay)]
|
||||
[BitAutoData(PaymentMethodType.BitPay)]
|
||||
[BitAutoData(PaymentMethodType.Credit)]
|
||||
[BitAutoData(PaymentMethodType.WireTransfer)]
|
||||
[BitAutoData(PaymentMethodType.Check)]
|
||||
public async Task PurchaseOrganizationAsync_Invalid(PaymentMethodType paymentMethodType, SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||
() => sutProvider.Sut.PurchaseOrganizationAsync(null, paymentMethodType, null, null, 0, 0, false, null, false, -1, -1));
|
||||
|
||||
Assert.Equal("Payment method is not supported at this time.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_Stripe_ProviderOrg_Coupon_Add(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo, bool provider = true)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
|
||||
sutProvider
|
||||
.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
});
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.BaseServiceUri.CloudRegion
|
||||
.Returns("US");
|
||||
|
||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo, provider);
|
||||
|
||||
Assert.Null(result);
|
||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
||||
Assert.True(organization.Enabled);
|
||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
||||
|
||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
||||
c.Description == organization.BusinessName &&
|
||||
c.Email == organization.BillingEmail &&
|
||||
c.Source == paymentToken &&
|
||||
c.PaymentMethod == null &&
|
||||
c.Coupon == "msp-discount-35" &&
|
||||
c.Metadata.Count == 1 &&
|
||||
c.Metadata["region"] == "US" &&
|
||||
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
c.Address.City == taxInfo.BillingAddressCity &&
|
||||
c.Address.State == taxInfo.BillingAddressState &&
|
||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
||||
));
|
||||
|
||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
||||
s.Customer == "C-1" &&
|
||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
||||
s.Items.Count == 0
|
||||
));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_SM_Stripe_ProviderOrg_Coupon_Add(SutProvider<StripePaymentService> sutProvider, Organization organization,
|
||||
string paymentToken, TaxInfo taxInfo, bool provider = true)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
organization.UseSecretsManager = true;
|
||||
|
||||
sutProvider
|
||||
.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
|
||||
});
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.BaseServiceUri.CloudRegion
|
||||
.Returns("US");
|
||||
|
||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 1, 1,
|
||||
false, taxInfo, provider, 1, 1);
|
||||
|
||||
Assert.Null(result);
|
||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
||||
Assert.True(organization.Enabled);
|
||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
||||
|
||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
||||
c.Description == organization.BusinessName &&
|
||||
c.Email == organization.BillingEmail &&
|
||||
c.Source == paymentToken &&
|
||||
c.PaymentMethod == null &&
|
||||
c.Coupon == "msp-discount-35" &&
|
||||
c.Metadata.Count == 1 &&
|
||||
c.Metadata["region"] == "US" &&
|
||||
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
c.Address.City == taxInfo.BillingAddressCity &&
|
||||
c.Address.State == taxInfo.BillingAddressState &&
|
||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
||||
));
|
||||
|
||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
||||
s.Customer == "C-1" &&
|
||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
||||
s.Items.Count == 4
|
||||
));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_Stripe(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
organization.UseSecretsManager = true;
|
||||
|
||||
sutProvider
|
||||
.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
});
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.BaseServiceUri.CloudRegion
|
||||
.Returns("US");
|
||||
|
||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0
|
||||
, false, taxInfo, false, 8, 10);
|
||||
|
||||
Assert.Null(result);
|
||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
||||
Assert.True(organization.Enabled);
|
||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
||||
c.Description == organization.BusinessName &&
|
||||
c.Email == organization.BillingEmail &&
|
||||
c.Source == paymentToken &&
|
||||
c.PaymentMethod == null &&
|
||||
c.Metadata.Count == 1 &&
|
||||
c.Metadata["region"] == "US" &&
|
||||
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
||||
c.InvoiceSettings.CustomFields != null &&
|
||||
c.InvoiceSettings.CustomFields[0].Name == "Organization" &&
|
||||
c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) &&
|
||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
c.Address.City == taxInfo.BillingAddressCity &&
|
||||
c.Address.State == taxInfo.BillingAddressState &&
|
||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
||||
));
|
||||
|
||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
||||
s.Customer == "C-1" &&
|
||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
||||
s.Items.Count == 2
|
||||
));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_Stripe_PM(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
paymentToken = "pm_" + paymentToken;
|
||||
|
||||
sutProvider
|
||||
.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
});
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.BaseServiceUri.CloudRegion
|
||||
.Returns("US");
|
||||
|
||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo);
|
||||
|
||||
Assert.Null(result);
|
||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
||||
Assert.True(organization.Enabled);
|
||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
||||
|
||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
||||
c.Description == organization.BusinessName &&
|
||||
c.Email == organization.BillingEmail &&
|
||||
c.Source == null &&
|
||||
c.PaymentMethod == paymentToken &&
|
||||
c.Metadata.Count == 1 &&
|
||||
c.Metadata["region"] == "US" &&
|
||||
c.InvoiceSettings.DefaultPaymentMethod == paymentToken &&
|
||||
c.InvoiceSettings.CustomFields != null &&
|
||||
c.InvoiceSettings.CustomFields[0].Name == "Organization" &&
|
||||
c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) &&
|
||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
c.Address.City == taxInfo.BillingAddressCity &&
|
||||
c.Address.State == taxInfo.BillingAddressState &&
|
||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
||||
));
|
||||
|
||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
||||
s.Customer == "C-1" &&
|
||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
||||
s.Items.Count == 0
|
||||
));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_Stripe_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
paymentToken = "pm_" + paymentToken;
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
Status = "incomplete",
|
||||
LatestInvoice = new Stripe.Invoice
|
||||
{
|
||||
PaymentIntent = new Stripe.PaymentIntent
|
||||
{
|
||||
Status = "requires_payment_method",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo));
|
||||
|
||||
Assert.Equal("Payment method was declined.", exception.Message);
|
||||
|
||||
await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_SM_Stripe_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
paymentToken = "pm_" + paymentToken;
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
Status = "incomplete",
|
||||
LatestInvoice = new Stripe.Invoice
|
||||
{
|
||||
PaymentIntent = new Stripe.PaymentIntent
|
||||
{
|
||||
Status = "requires_payment_method",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan,
|
||||
1, 12, false, taxInfo, false, 10, 10));
|
||||
|
||||
Assert.Equal("Payment method was declined.", exception.Message);
|
||||
|
||||
await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_Stripe_RequiresAction(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
Status = "incomplete",
|
||||
LatestInvoice = new Stripe.Invoice
|
||||
{
|
||||
PaymentIntent = new Stripe.PaymentIntent
|
||||
{
|
||||
Status = "requires_action",
|
||||
ClientSecret = "clientSecret",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo);
|
||||
|
||||
Assert.Equal("clientSecret", result);
|
||||
Assert.False(organization.Enabled);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_SM_Stripe_RequiresAction(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
Status = "incomplete",
|
||||
LatestInvoice = new Stripe.Invoice
|
||||
{
|
||||
PaymentIntent = new Stripe.PaymentIntent
|
||||
{
|
||||
Status = "requires_action",
|
||||
ClientSecret = "clientSecret",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan,
|
||||
10, 10, false, taxInfo, false, 10, 10);
|
||||
|
||||
Assert.Equal("clientSecret", result);
|
||||
Assert.False(organization.Enabled);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_Paypal(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
|
||||
sutProvider
|
||||
.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.BaseServiceUri.CloudRegion
|
||||
.Returns("US");
|
||||
|
||||
var customer = Substitute.For<Customer>();
|
||||
customer.Id.ReturnsForAnyArgs("Braintree-Id");
|
||||
customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For<PaymentMethod>() });
|
||||
var customerResult = Substitute.For<Result<Customer>>();
|
||||
customerResult.IsSuccess().Returns(true);
|
||||
customerResult.Target.ReturnsForAnyArgs(customer);
|
||||
|
||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
||||
|
||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo);
|
||||
|
||||
Assert.Null(result);
|
||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
||||
Assert.True(organization.Enabled);
|
||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
||||
|
||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
||||
c.Description == organization.BusinessName &&
|
||||
c.Email == organization.BillingEmail &&
|
||||
c.PaymentMethod == null &&
|
||||
c.Metadata.Count == 2 &&
|
||||
c.Metadata["btCustomerId"] == "Braintree-Id" &&
|
||||
c.Metadata["region"] == "US" &&
|
||||
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
c.Address.City == taxInfo.BillingAddressCity &&
|
||||
c.Address.State == taxInfo.BillingAddressState &&
|
||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
||||
));
|
||||
|
||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
||||
s.Customer == "C-1" &&
|
||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
||||
s.Items.Count == 0
|
||||
));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_SM_Paypal(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
organization.UseSecretsManager = true;
|
||||
|
||||
sutProvider
|
||||
.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
});
|
||||
|
||||
var customer = Substitute.For<Customer>();
|
||||
customer.Id.ReturnsForAnyArgs("Braintree-Id");
|
||||
customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For<PaymentMethod>() });
|
||||
var customerResult = Substitute.For<Result<Customer>>();
|
||||
customerResult.IsSuccess().Returns(true);
|
||||
customerResult.Target.ReturnsForAnyArgs(customer);
|
||||
|
||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.BaseServiceUri.CloudRegion
|
||||
.Returns("US");
|
||||
|
||||
var additionalStorage = (short)2;
|
||||
var additionalSeats = 10;
|
||||
var additionalSmSeats = 5;
|
||||
var additionalServiceAccounts = 20;
|
||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan,
|
||||
additionalStorage, additionalSeats, false, taxInfo, false, additionalSmSeats, additionalServiceAccounts);
|
||||
|
||||
Assert.Null(result);
|
||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
||||
Assert.True(organization.Enabled);
|
||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
||||
|
||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
||||
c.Description == organization.BusinessName &&
|
||||
c.Email == organization.BillingEmail &&
|
||||
c.PaymentMethod == null &&
|
||||
c.Metadata.Count == 2 &&
|
||||
c.Metadata["region"] == "US" &&
|
||||
c.Metadata["btCustomerId"] == "Braintree-Id" &&
|
||||
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
c.Address.City == taxInfo.BillingAddressCity &&
|
||||
c.Address.State == taxInfo.BillingAddressState &&
|
||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
||||
));
|
||||
|
||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
||||
s.Customer == "C-1" &&
|
||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
||||
s.Items.Count == 4 &&
|
||||
s.Items.Count(i => i.Plan == plan.PasswordManager.StripeSeatPlanId && i.Quantity == additionalSeats) == 1 &&
|
||||
s.Items.Count(i => i.Plan == plan.PasswordManager.StripeStoragePlanId && i.Quantity == additionalStorage) == 1 &&
|
||||
s.Items.Count(i => i.Plan == plan.SecretsManager.StripeSeatPlanId && i.Quantity == additionalSmSeats) == 1 &&
|
||||
s.Items.Count(i => i.Plan == plan.SecretsManager.StripeServiceAccountPlanId && i.Quantity == additionalServiceAccounts) == 1
|
||||
));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_Paypal_FailedCreate(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
|
||||
var customerResult = Substitute.For<Result<Customer>>();
|
||||
customerResult.IsSuccess().Returns(false);
|
||||
|
||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo));
|
||||
|
||||
Assert.Equal("Failed to create PayPal customer record.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_SM_Paypal_FailedCreate(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
|
||||
var customerResult = Substitute.For<Result<Customer>>();
|
||||
customerResult.IsSuccess().Returns(false);
|
||||
|
||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan,
|
||||
1, 1, false, taxInfo, false, 8, 8));
|
||||
|
||||
Assert.Equal("Failed to create PayPal customer record.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PurchaseOrganizationAsync_PayPal_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
var plans = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
paymentToken = "pm_" + paymentToken;
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
Status = "incomplete",
|
||||
LatestInvoice = new Stripe.Invoice
|
||||
{
|
||||
PaymentIntent = new Stripe.PaymentIntent
|
||||
{
|
||||
Status = "requires_payment_method",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var customer = Substitute.For<Customer>();
|
||||
customer.Id.ReturnsForAnyArgs("Braintree-Id");
|
||||
customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For<PaymentMethod>() });
|
||||
var customerResult = Substitute.For<Result<Customer>>();
|
||||
customerResult.IsSuccess().Returns(true);
|
||||
customerResult.Target.ReturnsForAnyArgs(customer);
|
||||
|
||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plans, 0, 0, false, taxInfo));
|
||||
|
||||
Assert.Equal("Payment method was declined.", exception.Message);
|
||||
|
||||
await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
|
||||
await braintreeGateway.Customer.Received(1).DeleteAsync("Braintree-Id");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("ES", "A5372895732985327895237")]
|
||||
public async Task PurchaseOrganizationAsync_ThrowsBadRequestException_WhenTaxIdInvalid(string country, string taxId, SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||
{
|
||||
taxInfo.BillingAddressCountry = country;
|
||||
taxInfo.TaxIdNumber = taxId;
|
||||
taxInfo.TaxIdType = null;
|
||||
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
organization.UseSecretsManager = true;
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||
{
|
||||
Id = "S-1",
|
||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||
});
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.BaseServiceUri.CloudRegion
|
||||
.Returns("US");
|
||||
sutProvider
|
||||
.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(p => p == country), Arg.Is<string>(p => p == taxId))
|
||||
.Returns((string)null);
|
||||
|
||||
var actual = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo, false, 8, 10));
|
||||
|
||||
Assert.Equal("billingTaxIdTypeInferenceError", actual.Message);
|
||||
|
||||
await stripeAdapter.Received(0).CustomerCreateAsync(Arg.Any<Stripe.CustomerCreateOptions>());
|
||||
await stripeAdapter.Received(0).SubscriptionCreateAsync(Arg.Any<Stripe.SubscriptionCreateOptions>());
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpgradeFreeOrganizationAsync_Success(SutProvider<StripePaymentService> sutProvider,
|
||||
Organization organization, TaxInfo taxInfo)
|
||||
{
|
||||
organization.GatewaySubscriptionId = null;
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "btCustomerId", "B-123" },
|
||||
}
|
||||
});
|
||||
stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "btCustomerId", "B-123" },
|
||||
}
|
||||
});
|
||||
stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice
|
||||
{
|
||||
PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", },
|
||||
AmountDue = 0
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
|
||||
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
|
||||
var upgrade = new OrganizationUpgrade()
|
||||
{
|
||||
AdditionalStorageGb = 0,
|
||||
AdditionalSeats = 0,
|
||||
PremiumAccessAddon = false,
|
||||
TaxInfo = taxInfo,
|
||||
AdditionalSmSeats = 0,
|
||||
AdditionalServiceAccounts = 0
|
||||
};
|
||||
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpgradeFreeOrganizationAsync_SM_Success(SutProvider<StripePaymentService> sutProvider,
|
||||
Organization organization, TaxInfo taxInfo)
|
||||
{
|
||||
organization.GatewaySubscriptionId = null;
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "btCustomerId", "B-123" },
|
||||
}
|
||||
});
|
||||
stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "btCustomerId", "B-123" },
|
||||
}
|
||||
});
|
||||
stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice
|
||||
{
|
||||
PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", },
|
||||
AmountDue = 0
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
|
||||
|
||||
var upgrade = new OrganizationUpgrade()
|
||||
{
|
||||
AdditionalStorageGb = 1,
|
||||
AdditionalSeats = 10,
|
||||
PremiumAccessAddon = false,
|
||||
TaxInfo = taxInfo,
|
||||
AdditionalSmSeats = 5,
|
||||
AdditionalServiceAccounts = 50
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpgradeFreeOrganizationAsync_WhenCustomerHasNoAddress_UpdatesCustomerAddressWithTaxInfo(
|
||||
SutProvider<StripePaymentService> sutProvider,
|
||||
Organization organization,
|
||||
TaxInfo taxInfo)
|
||||
{
|
||||
organization.GatewaySubscriptionId = null;
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
||||
stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "btCustomerId", "B-123" },
|
||||
}
|
||||
});
|
||||
stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||
{
|
||||
Id = "C-1",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "btCustomerId", "B-123" },
|
||||
}
|
||||
});
|
||||
stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice
|
||||
{
|
||||
PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", },
|
||||
AmountDue = 0
|
||||
});
|
||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
|
||||
|
||||
var upgrade = new OrganizationUpgrade()
|
||||
{
|
||||
AdditionalStorageGb = 1,
|
||||
AdditionalSeats = 10,
|
||||
PremiumAccessAddon = false,
|
||||
TaxInfo = taxInfo,
|
||||
AdditionalSmSeats = 5,
|
||||
AdditionalServiceAccounts = 50
|
||||
};
|
||||
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
_ = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade);
|
||||
|
||||
await stripeAdapter.Received()
|
||||
.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<Stripe.CustomerUpdateOptions>(c =>
|
||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||
c.Address.City == taxInfo.BillingAddressCity &&
|
||||
c.Address.State == taxInfo.BillingAddressState));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user