You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1893 lines
77 KiB
1893 lines
77 KiB
using Bit.Billing.Models; |
|
using Bit.Core.Entities; |
|
using Bit.Core.Enums; |
|
using Bit.Core.Exceptions; |
|
using Bit.Core.Models.BitStripe; |
|
using Bit.Core.Models.Business; |
|
using Bit.Core.Repositories; |
|
using Bit.Core.Settings; |
|
using Microsoft.Extensions.Logging; |
|
using StaticStore = Bit.Core.Models.StaticStore; |
|
using TaxRate = Bit.Core.Entities.TaxRate; |
|
|
|
namespace Bit.Core.Services; |
|
|
|
public class StripePaymentService : IPaymentService |
|
{ |
|
private const string PremiumPlanId = "premium-annually"; |
|
private const string PremiumPlanAppleIapId = "premium-annually-appleiap"; |
|
private const decimal PremiumPlanAppleIapPrice = 14.99M; |
|
private const string StoragePlanId = "storage-gb-annually"; |
|
private const string ProviderDiscountId = "msp-discount-35"; |
|
|
|
private readonly ITransactionRepository _transactionRepository; |
|
private readonly IUserRepository _userRepository; |
|
private readonly IAppleIapService _appleIapService; |
|
private readonly ILogger<StripePaymentService> _logger; |
|
private readonly Braintree.IBraintreeGateway _btGateway; |
|
private readonly ITaxRateRepository _taxRateRepository; |
|
private readonly IStripeAdapter _stripeAdapter; |
|
private readonly IGlobalSettings _globalSettings; |
|
|
|
public StripePaymentService( |
|
ITransactionRepository transactionRepository, |
|
IUserRepository userRepository, |
|
IAppleIapService appleIapService, |
|
ILogger<StripePaymentService> logger, |
|
ITaxRateRepository taxRateRepository, |
|
IStripeAdapter stripeAdapter, |
|
Braintree.IBraintreeGateway braintreeGateway, |
|
IGlobalSettings globalSettings) |
|
{ |
|
_transactionRepository = transactionRepository; |
|
_userRepository = userRepository; |
|
_appleIapService = appleIapService; |
|
_logger = logger; |
|
_taxRateRepository = taxRateRepository; |
|
_stripeAdapter = stripeAdapter; |
|
_btGateway = braintreeGateway; |
|
_globalSettings = globalSettings; |
|
} |
|
|
|
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) |
|
{ |
|
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."); |
|
} |
|
|
|
if (taxInfo != null && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressCountry) && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressPostalCode)) |
|
{ |
|
var taxRateSearch = new TaxRate |
|
{ |
|
Country = taxInfo.BillingAddressCountry, |
|
PostalCode = taxInfo.BillingAddressPostalCode |
|
}; |
|
var taxRates = await _taxRateRepository.GetByLocationAsync(taxRateSearch); |
|
|
|
// should only be one tax rate per country/zip combo |
|
var taxRate = taxRates.FirstOrDefault(); |
|
if (taxRate != null) |
|
{ |
|
taxInfo.StripeTaxRateId = taxRate.Id; |
|
} |
|
} |
|
|
|
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon |
|
, additionalSmSeats, additionalServiceAccount); |
|
|
|
Stripe.Customer customer = null; |
|
Stripe.Subscription subscription; |
|
try |
|
{ |
|
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions |
|
{ |
|
Description = org.BusinessName, |
|
Email = org.BillingEmail, |
|
Source = stipeCustomerSourceToken, |
|
PaymentMethod = stipeCustomerPaymentMethodId, |
|
Metadata = stripeCustomerMetadata, |
|
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions |
|
{ |
|
DefaultPaymentMethod = stipeCustomerPaymentMethodId, |
|
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions> |
|
{ |
|
new Stripe.CustomerInvoiceSettingsCustomFieldOptions() |
|
{ |
|
Name = org.SubscriberType(), |
|
Value = GetFirstThirtyCharacters(org.SubscriberName()), |
|
}, |
|
}, |
|
}, |
|
Coupon = provider ? ProviderDiscountId : null, |
|
Address = new Stripe.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 = !taxInfo.HasTaxId ? null : new List<Stripe.CustomerTaxIdDataOptions> |
|
{ |
|
new Stripe.CustomerTaxIdDataOptions |
|
{ |
|
Type = taxInfo.TaxIdType, |
|
Value = taxInfo.TaxIdNumber, |
|
}, |
|
}, |
|
}); |
|
subCreateOptions.AddExpand("latest_invoice.payment_intent"); |
|
subCreateOptions.Customer = customer.Id; |
|
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 Stripe.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; |
|
} |
|
} |
|
|
|
private async Task ChangeOrganizationSponsorship(Organization org, OrganizationSponsorship sponsorship, bool applySponsorship) |
|
{ |
|
var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType); |
|
var sponsoredPlan = sponsorship != null ? |
|
Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) : |
|
null; |
|
var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); |
|
|
|
await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, DateTime.UtcNow); |
|
|
|
var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); |
|
org.ExpirationDate = sub.CurrentPeriodEnd; |
|
sponsorship.ValidUntil = sub.CurrentPeriodEnd; |
|
|
|
} |
|
|
|
public Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship) => |
|
ChangeOrganizationSponsorship(org, sponsorship, true); |
|
|
|
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 Stripe.CustomerGetOptions(); |
|
customerOptions.AddExpand("default_source"); |
|
customerOptions.AddExpand("invoice_settings.default_payment_method"); |
|
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions); |
|
if (customer == null) |
|
{ |
|
throw new GatewayException("Could not find customer payment profile."); |
|
} |
|
|
|
var taxInfo = upgrade.TaxInfo; |
|
if (taxInfo != null && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressCountry) && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressPostalCode)) |
|
{ |
|
var taxRateSearch = new TaxRate |
|
{ |
|
Country = taxInfo.BillingAddressCountry, |
|
PostalCode = taxInfo.BillingAddressPostalCode |
|
}; |
|
var taxRates = await _taxRateRepository.GetByLocationAsync(taxRateSearch); |
|
|
|
// should only be one tax rate per country/zip combo |
|
var taxRate = taxRates.FirstOrDefault(); |
|
if (taxRate != null) |
|
{ |
|
taxInfo.StripeTaxRateId = taxRate.Id; |
|
} |
|
} |
|
|
|
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade); |
|
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( |
|
Stripe.Customer customer, Stripe.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 Stripe.Card || customer.DefaultSource is Stripe.SourceCard) |
|
{ |
|
paymentMethodType = PaymentMethodType.Card; |
|
stripePaymentMethod = true; |
|
} |
|
else if (customer.DefaultSource is Stripe.BankAccount || customer.DefaultSource is Stripe.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 == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.GoogleInApp) |
|
{ |
|
throw new GatewayException("Payment method is not supported at this time."); |
|
} |
|
if ((paymentMethodType == PaymentMethodType.GoogleInApp || |
|
paymentMethodType == PaymentMethodType.AppleInApp) && additionalStorageGb > 0) |
|
{ |
|
throw new BadRequestException("You cannot add storage with this payment method."); |
|
} |
|
|
|
var createdStripeCustomer = false; |
|
Stripe.Customer customer = null; |
|
Braintree.Customer braintreeCustomer = null; |
|
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card || |
|
paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == 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)) |
|
{ |
|
try |
|
{ |
|
await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, true, taxInfo); |
|
} |
|
catch (Exception e) |
|
{ |
|
var message = e.Message.ToLowerInvariant(); |
|
if (message.Contains("apple") || message.Contains("in-app")) |
|
{ |
|
throw; |
|
} |
|
} |
|
} |
|
try |
|
{ |
|
customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId); |
|
} |
|
catch { } |
|
} |
|
|
|
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 (paymentMethodType == PaymentMethodType.AppleInApp) |
|
{ |
|
var verifiedReceiptStatus = await _appleIapService.GetVerifiedReceiptStatusAsync(paymentToken); |
|
if (verifiedReceiptStatus == null) |
|
{ |
|
throw new GatewayException("Cannot verify apple in-app purchase."); |
|
} |
|
var receiptOriginalTransactionId = verifiedReceiptStatus.GetOriginalTransactionId(); |
|
await VerifyAppleReceiptNotInUseAsync(receiptOriginalTransactionId, user); |
|
await _appleIapService.SaveReceiptAsync(verifiedReceiptStatus, user.Id); |
|
stripeCustomerMetadata.Add("appleReceipt", receiptOriginalTransactionId); |
|
} |
|
else if (!stripePaymentMethod) |
|
{ |
|
throw new GatewayException("Payment method is not supported at this time."); |
|
} |
|
|
|
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions |
|
{ |
|
Description = user.Name, |
|
Email = user.Email, |
|
Metadata = stripeCustomerMetadata, |
|
PaymentMethod = stipeCustomerPaymentMethodId, |
|
Source = stipeCustomerSourceToken, |
|
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions |
|
{ |
|
DefaultPaymentMethod = stipeCustomerPaymentMethodId, |
|
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions> |
|
{ |
|
new Stripe.CustomerInvoiceSettingsCustomFieldOptions() |
|
{ |
|
Name = user.SubscriberType(), |
|
Value = GetFirstThirtyCharacters(user.SubscriberName()), |
|
}, |
|
} |
|
}, |
|
Address = new Stripe.AddressOptions |
|
{ |
|
Line1 = string.Empty, |
|
Country = taxInfo.BillingAddressCountry, |
|
PostalCode = taxInfo.BillingAddressPostalCode, |
|
}, |
|
}); |
|
createdStripeCustomer = true; |
|
} |
|
|
|
if (customer == null) |
|
{ |
|
throw new GatewayException("Could not set up customer payment profile."); |
|
} |
|
|
|
var subCreateOptions = new Stripe.SubscriptionCreateOptions |
|
{ |
|
Customer = customer.Id, |
|
Items = new List<Stripe.SubscriptionItemOptions>(), |
|
Metadata = new Dictionary<string, string> |
|
{ |
|
[user.GatewayIdField()] = user.Id.ToString() |
|
} |
|
}; |
|
|
|
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions |
|
{ |
|
Plan = paymentMethodType == PaymentMethodType.AppleInApp ? PremiumPlanAppleIapId : PremiumPlanId, |
|
Quantity = 1, |
|
}); |
|
|
|
if (!string.IsNullOrWhiteSpace(taxInfo?.BillingAddressCountry) |
|
&& !string.IsNullOrWhiteSpace(taxInfo?.BillingAddressPostalCode)) |
|
{ |
|
var taxRates = await _taxRateRepository.GetByLocationAsync( |
|
new TaxRate() |
|
{ |
|
Country = taxInfo.BillingAddressCountry, |
|
PostalCode = taxInfo.BillingAddressPostalCode |
|
} |
|
); |
|
var taxRate = taxRates.FirstOrDefault(); |
|
if (taxRate != null) |
|
{ |
|
subCreateOptions.DefaultTaxRates = new List<string>(1) |
|
{ |
|
taxRate.Id |
|
}; |
|
} |
|
} |
|
|
|
if (additionalStorageGb > 0) |
|
{ |
|
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions |
|
{ |
|
Plan = StoragePlanId, |
|
Quantity = additionalStorageGb |
|
}); |
|
} |
|
|
|
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; |
|
} |
|
else |
|
{ |
|
user.Premium = true; |
|
user.PremiumExpirationDate = subscription.CurrentPeriodEnd; |
|
return null; |
|
} |
|
} |
|
|
|
private async Task<Stripe.Subscription> ChargeForNewSubscriptionAsync(ISubscriber subscriber, Stripe.Customer customer, |
|
bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType, |
|
Stripe.SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer) |
|
{ |
|
var addedCreditToStripeCustomer = false; |
|
Braintree.Transaction braintreeTransaction = null; |
|
Transaction appleTransaction = null; |
|
|
|
var subInvoiceMetadata = new Dictionary<string, string>(); |
|
Stripe.Subscription subscription = null; |
|
try |
|
{ |
|
if (!stripePaymentMethod) |
|
{ |
|
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions |
|
{ |
|
Customer = customer.Id, |
|
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items), |
|
SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates, |
|
}); |
|
|
|
if (previewInvoice.AmountDue > 0) |
|
{ |
|
var appleReceiptOrigTransactionId = customer.Metadata != null && |
|
customer.Metadata.ContainsKey("appleReceipt") ? customer.Metadata["appleReceipt"] : null; |
|
var braintreeCustomerId = customer.Metadata != null && |
|
customer.Metadata.ContainsKey("btCustomerId") ? customer.Metadata["btCustomerId"] : null; |
|
if (!string.IsNullOrWhiteSpace(appleReceiptOrigTransactionId)) |
|
{ |
|
if (!subscriber.IsUser()) |
|
{ |
|
throw new GatewayException("In-app purchase is only allowed for users."); |
|
} |
|
|
|
var appleReceipt = await _appleIapService.GetReceiptAsync( |
|
appleReceiptOrigTransactionId); |
|
var verifiedAppleReceipt = await _appleIapService.GetVerifiedReceiptStatusAsync( |
|
appleReceipt.Item1); |
|
if (verifiedAppleReceipt == null) |
|
{ |
|
throw new GatewayException("Failed to get Apple in-app purchase receipt data."); |
|
} |
|
subInvoiceMetadata.Add("appleReceipt", verifiedAppleReceipt.GetOriginalTransactionId()); |
|
var lastTransactionId = verifiedAppleReceipt.GetLastTransactionId(); |
|
subInvoiceMetadata.Add("appleReceiptTransactionId", lastTransactionId); |
|
var existingTransaction = await _transactionRepository.GetByGatewayIdAsync( |
|
GatewayType.AppStore, lastTransactionId); |
|
if (existingTransaction == null) |
|
{ |
|
appleTransaction = verifiedAppleReceipt.BuildTransactionFromLastTransaction( |
|
PremiumPlanAppleIapPrice, subscriber.Id); |
|
appleTransaction.Type = TransactionType.Charge; |
|
await _transactionRepository.CreateAsync(appleTransaction); |
|
} |
|
} |
|
else 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 Stripe.CustomerUpdateOptions |
|
{ |
|
Balance = customer.Balance - previewInvoice.AmountDue |
|
}); |
|
addedCreditToStripeCustomer = true; |
|
} |
|
} |
|
else if (paymentMethodType == PaymentMethodType.Credit) |
|
{ |
|
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions |
|
{ |
|
Customer = customer.Id, |
|
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items), |
|
SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates, |
|
}); |
|
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 Stripe.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 Stripe.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 Stripe.CustomerUpdateOptions |
|
{ |
|
Balance = customer.Balance |
|
}); |
|
} |
|
} |
|
if (braintreeTransaction != null) |
|
{ |
|
await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id); |
|
} |
|
if (braintreeCustomer != null) |
|
{ |
|
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); |
|
} |
|
if (appleTransaction != null) |
|
{ |
|
await _transactionRepository.DeleteAsync(appleTransaction); |
|
} |
|
|
|
if (e is Stripe.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<Stripe.InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions( |
|
List<Stripe.SubscriptionItemOptions> subItemOptions) |
|
{ |
|
return subItemOptions.Select(si => new Stripe.InvoiceSubscriptionItemOptions |
|
{ |
|
Plan = si.Plan, |
|
Quantity = si.Quantity |
|
}).ToList(); |
|
} |
|
|
|
private async Task<string> FinalizeSubscriptionChangeAsync(IStorableSubscriber storableSubscriber, |
|
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate) |
|
{ |
|
// remember, when in doubt, throw |
|
|
|
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId); |
|
if (sub == null) |
|
{ |
|
throw new GatewayException("Subscription not found."); |
|
} |
|
|
|
prorationDate ??= DateTime.UtcNow; |
|
var collectionMethod = sub.CollectionMethod; |
|
var daysUntilDue = sub.DaysUntilDue; |
|
var chargeNow = collectionMethod == "charge_automatically"; |
|
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub); |
|
|
|
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions |
|
{ |
|
Items = updatedItemOptions, |
|
ProrationBehavior = "always_invoice", |
|
DaysUntilDue = daysUntilDue ?? 1, |
|
CollectionMethod = "send_invoice", |
|
ProrationDate = prorationDate, |
|
}; |
|
|
|
if (!subscriptionUpdate.UpdateNeeded(sub)) |
|
{ |
|
// No need to update subscription, quantity matches |
|
return null; |
|
} |
|
|
|
var customer = await _stripeAdapter.CustomerGetAsync(sub.CustomerId); |
|
|
|
if (!string.IsNullOrWhiteSpace(customer?.Address?.Country) |
|
&& !string.IsNullOrWhiteSpace(customer?.Address?.PostalCode)) |
|
{ |
|
var taxRates = await _taxRateRepository.GetByLocationAsync( |
|
new TaxRate() |
|
{ |
|
Country = customer.Address.Country, |
|
PostalCode = customer.Address.PostalCode |
|
} |
|
); |
|
var taxRate = taxRates.FirstOrDefault(); |
|
if (taxRate != null && !sub.DefaultTaxRates.Any(x => x.Equals(taxRate.Id))) |
|
{ |
|
subUpdateOptions.DefaultTaxRates = new List<string>(1) |
|
{ |
|
taxRate.Id |
|
}; |
|
} |
|
} |
|
|
|
string paymentIntentClientSecret = null; |
|
try |
|
{ |
|
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions); |
|
|
|
var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions()); |
|
if (invoice == null) |
|
{ |
|
throw new BadRequestException("Unable to locate draft invoice for subscription update."); |
|
} |
|
|
|
if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0)) |
|
{ |
|
try |
|
{ |
|
if (chargeNow) |
|
{ |
|
paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync( |
|
storableSubscriber, invoice); |
|
} |
|
else |
|
{ |
|
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions |
|
{ |
|
AutoAdvance = false, |
|
}); |
|
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions()); |
|
paymentIntentClientSecret = null; |
|
} |
|
} |
|
catch |
|
{ |
|
// Need to revert the subscription |
|
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions |
|
{ |
|
Items = subscriptionUpdate.RevertItemsOptions(sub), |
|
// This proration behavior prevents a false "credit" from |
|
// being applied forward to the next month's invoice |
|
ProrationBehavior = "none", |
|
CollectionMethod = collectionMethod, |
|
DaysUntilDue = daysUntilDue, |
|
}); |
|
throw; |
|
} |
|
} |
|
else if (!invoice.Paid) |
|
{ |
|
// Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h |
|
invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId); |
|
paymentIntentClientSecret = null; |
|
} |
|
|
|
} |
|
finally |
|
{ |
|
// Change back the subscription collection method and/or days until due |
|
if (collectionMethod != "send_invoice" || daysUntilDue == null) |
|
{ |
|
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions |
|
{ |
|
CollectionMethod = collectionMethod, |
|
DaysUntilDue = daysUntilDue, |
|
}); |
|
} |
|
} |
|
|
|
return paymentIntentClientSecret; |
|
} |
|
|
|
public Task<string> AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null) |
|
{ |
|
return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate); |
|
} |
|
|
|
public Task<string> AdjustSmSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null) |
|
{ |
|
return FinalizeSubscriptionChangeAsync(organization, new SmSeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate); |
|
} |
|
|
|
public Task<string> AdjustServiceAccountsAsync(Organization organization, StaticStore.Plan plan, int additionalServiceAccounts, DateTime? prorationDate = null) |
|
{ |
|
return FinalizeSubscriptionChangeAsync(organization, new ServiceAccountSubscriptionUpdate(organization, plan, additionalServiceAccounts), prorationDate); |
|
} |
|
|
|
public Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, |
|
string storagePlanId, DateTime? prorationDate = null) |
|
{ |
|
return FinalizeSubscriptionChangeAsync(storableSubscriber, new StorageSubscriptionUpdate(storagePlanId, additionalStorage), prorationDate); |
|
} |
|
|
|
public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber) |
|
{ |
|
if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) |
|
{ |
|
await _stripeAdapter.SubscriptionCancelAsync(subscriber.GatewaySubscriptionId, |
|
new Stripe.SubscriptionCancelOptions()); |
|
} |
|
|
|
if (string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) |
|
{ |
|
return; |
|
} |
|
|
|
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId); |
|
if (customer == null) |
|
{ |
|
return; |
|
} |
|
|
|
if (customer.Metadata.ContainsKey("btCustomerId")) |
|
{ |
|
var transactionRequest = new Braintree.TransactionSearchRequest() |
|
.CustomerId.Is(customer.Metadata["btCustomerId"]); |
|
var transactions = _btGateway.Transaction.Search(transactionRequest); |
|
|
|
if ((transactions?.MaximumCount ?? 0) > 0) |
|
{ |
|
var txs = transactions.Cast<Braintree.Transaction>().Where(c => c.RefundedTransactionId == null); |
|
foreach (var transaction in txs) |
|
{ |
|
await _btGateway.Transaction.RefundAsync(transaction.Id); |
|
} |
|
} |
|
|
|
await _btGateway.Customer.DeleteAsync(customer.Metadata["btCustomerId"]); |
|
} |
|
else |
|
{ |
|
var charges = await _stripeAdapter.ChargeListAsync(new Stripe.ChargeListOptions |
|
{ |
|
Customer = subscriber.GatewayCustomerId |
|
}); |
|
|
|
if (charges?.Data != null) |
|
{ |
|
foreach (var charge in charges.Data.Where(c => c.Captured && !c.Refunded)) |
|
{ |
|
await _stripeAdapter.RefundCreateAsync(new Stripe.RefundCreateOptions { Charge = charge.Id }); |
|
} |
|
} |
|
} |
|
|
|
await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId); |
|
} |
|
|
|
public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Stripe.Invoice invoice) |
|
{ |
|
var customerOptions = new Stripe.CustomerGetOptions(); |
|
customerOptions.AddExpand("default_source"); |
|
customerOptions.AddExpand("invoice_settings.default_payment_method"); |
|
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions); |
|
var usingInAppPaymentMethod = customer.Metadata.ContainsKey("appleReceipt"); |
|
if (usingInAppPaymentMethod) |
|
{ |
|
throw new BadRequestException("Cannot perform this action with in-app purchase payment method. " + |
|
"Contact support."); |
|
} |
|
|
|
string paymentIntentClientSecret = null; |
|
|
|
// Invoice them and pay now instead of waiting until Stripe does this automatically. |
|
|
|
string cardPaymentMethodId = null; |
|
if (!customer.Metadata.ContainsKey("btCustomerId")) |
|
{ |
|
var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card"; |
|
var hasDefaultValidSource = customer.DefaultSource != null && |
|
(customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount); |
|
if (!hasDefaultCardPaymentMethod && !hasDefaultValidSource) |
|
{ |
|
cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id; |
|
if (cardPaymentMethodId == null) |
|
{ |
|
// We're going to delete this draft invoice, it can't be paid |
|
try |
|
{ |
|
await _stripeAdapter.InvoiceDeleteAsync(invoice.Id); |
|
} |
|
catch |
|
{ |
|
await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions |
|
{ |
|
AutoAdvance = false |
|
}); |
|
await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id); |
|
} |
|
throw new BadRequestException("No payment method is available."); |
|
} |
|
} |
|
} |
|
|
|
Braintree.Transaction braintreeTransaction = null; |
|
try |
|
{ |
|
// Finalize the invoice (from Draft) w/o auto-advance so we |
|
// can attempt payment manually. |
|
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions |
|
{ |
|
AutoAdvance = false, |
|
}); |
|
var invoicePayOptions = new Stripe.InvoicePayOptions |
|
{ |
|
PaymentMethod = cardPaymentMethodId, |
|
}; |
|
if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false) |
|
{ |
|
invoicePayOptions.PaidOutOfBand = true; |
|
var btInvoiceAmount = (invoice.AmountDue / 100M); |
|
var transactionResult = await _btGateway.Transaction.SaleAsync( |
|
new Braintree.TransactionRequest |
|
{ |
|
Amount = btInvoiceAmount, |
|
CustomerId = customer.Metadata["btCustomerId"], |
|
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; |
|
invoice = await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions |
|
{ |
|
Metadata = new Dictionary<string, string> |
|
{ |
|
["btTransactionId"] = braintreeTransaction.Id, |
|
["btPayPalTransactionId"] = |
|
braintreeTransaction.PayPalDetails.AuthorizationId |
|
}, |
|
}); |
|
invoicePayOptions.PaidOutOfBand = true; |
|
} |
|
|
|
try |
|
{ |
|
invoice = await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions); |
|
} |
|
catch (Stripe.StripeException e) |
|
{ |
|
if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired && |
|
e.StripeError?.Code == "invoice_payment_intent_requires_action") |
|
{ |
|
// SCA required, get intent client secret |
|
var invoiceGetOptions = new Stripe.InvoiceGetOptions(); |
|
invoiceGetOptions.AddExpand("payment_intent"); |
|
invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions); |
|
paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret; |
|
} |
|
else |
|
{ |
|
throw new GatewayException("Unable to pay invoice."); |
|
} |
|
} |
|
} |
|
catch (Exception e) |
|
{ |
|
if (braintreeTransaction != null) |
|
{ |
|
await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id); |
|
} |
|
if (invoice != null) |
|
{ |
|
if (invoice.Status == "paid") |
|
{ |
|
// It's apparently paid, so we need to return w/o throwing an exception |
|
return paymentIntentClientSecret; |
|
} |
|
|
|
invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new Stripe.InvoiceVoidOptions()); |
|
|
|
// HACK: Workaround for customer balance credit |
|
if (invoice.StartingBalance < 0) |
|
{ |
|
// Customer had a balance applied to this invoice. Since we can't fully trust Stripe to |
|
// credit it back to the customer (even though their docs claim they will), we need to |
|
// check that balance against the current customer balance and determine if it needs to be re-applied |
|
customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions); |
|
|
|
// Assumption: Customer balance should now be $0, otherwise payment would not have failed. |
|
if (customer.Balance == 0) |
|
{ |
|
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions |
|
{ |
|
Balance = invoice.StartingBalance |
|
}); |
|
} |
|
} |
|
} |
|
|
|
if (e is Stripe.StripeException strEx && |
|
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false)) |
|
{ |
|
throw new GatewayException("Bank account is not yet verified."); |
|
} |
|
|
|
// Let the caller perform any subscription change cleanup |
|
throw; |
|
} |
|
return paymentIntentClientSecret; |
|
} |
|
|
|
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false, |
|
bool skipInAppPurchaseCheck = false) |
|
{ |
|
if (subscriber == null) |
|
{ |
|
throw new ArgumentNullException(nameof(subscriber)); |
|
} |
|
|
|
if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) |
|
{ |
|
throw new GatewayException("No subscription."); |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId) && !skipInAppPurchaseCheck) |
|
{ |
|
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId); |
|
if (customer.Metadata.ContainsKey("appleReceipt")) |
|
{ |
|
throw new BadRequestException("You are required to manage your subscription from the app store."); |
|
} |
|
} |
|
|
|
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId); |
|
if (sub == null) |
|
{ |
|
throw new GatewayException("Subscription was not found."); |
|
} |
|
|
|
if (sub.CanceledAt.HasValue || sub.Status == "canceled" || sub.Status == "unpaid" || |
|
sub.Status == "incomplete_expired") |
|
{ |
|
// Already canceled |
|
return; |
|
} |
|
|
|
try |
|
{ |
|
var canceledSub = endOfPeriod ? |
|
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, |
|
new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) : |
|
await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new Stripe.SubscriptionCancelOptions()); |
|
if (!canceledSub.CanceledAt.HasValue) |
|
{ |
|
throw new GatewayException("Unable to cancel subscription."); |
|
} |
|
} |
|
catch (Stripe.StripeException e) |
|
{ |
|
if (e.Message != $"No such subscription: {subscriber.GatewaySubscriptionId}") |
|
{ |
|
throw; |
|
} |
|
} |
|
} |
|
|
|
public async Task ReinstateSubscriptionAsync(ISubscriber subscriber) |
|
{ |
|
if (subscriber == null) |
|
{ |
|
throw new ArgumentNullException(nameof(subscriber)); |
|
} |
|
|
|
if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) |
|
{ |
|
throw new GatewayException("No subscription."); |
|
} |
|
|
|
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId); |
|
if (sub == null) |
|
{ |
|
throw new GatewayException("Subscription was not found."); |
|
} |
|
|
|
if ((sub.Status != "active" && sub.Status != "trialing" && !sub.Status.StartsWith("incomplete")) || |
|
!sub.CanceledAt.HasValue) |
|
{ |
|
throw new GatewayException("Subscription is not marked for cancellation."); |
|
} |
|
|
|
var updatedSub = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, |
|
new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = false }); |
|
if (updatedSub.CanceledAt.HasValue) |
|
{ |
|
throw new GatewayException("Unable to reinstate subscription."); |
|
} |
|
} |
|
|
|
public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, |
|
string paymentToken, bool allowInAppPurchases = false, TaxInfo taxInfo = null) |
|
{ |
|
if (subscriber == null) |
|
{ |
|
throw new ArgumentNullException(nameof(subscriber)); |
|
} |
|
|
|
if (subscriber.Gateway.HasValue && subscriber.Gateway.Value != GatewayType.Stripe) |
|
{ |
|
throw new GatewayException("Switching from one payment type to another is not supported. " + |
|
"Contact us for assistance."); |
|
} |
|
|
|
var createdCustomer = false; |
|
AppleReceiptStatus appleReceiptStatus = null; |
|
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; |
|
var inAppPurchase = paymentMethodType == PaymentMethodType.AppleInApp || |
|
paymentMethodType == PaymentMethodType.GoogleInApp; |
|
|
|
Stripe.Customer customer = null; |
|
|
|
if (!allowInAppPurchases && inAppPurchase) |
|
{ |
|
throw new GatewayException("In-app purchase payment method is not allowed."); |
|
} |
|
|
|
if (!subscriber.IsUser() && inAppPurchase) |
|
{ |
|
throw new GatewayException("In-app purchase payment method is only allowed for users."); |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) |
|
{ |
|
var options = new Stripe.CustomerGetOptions(); |
|
options.AddExpand("sources"); |
|
customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, options); |
|
if (customer.Metadata?.Any() ?? false) |
|
{ |
|
stripeCustomerMetadata = customer.Metadata; |
|
} |
|
} |
|
|
|
if (inAppPurchase && customer != null && customer.Balance != 0) |
|
{ |
|
throw new GatewayException("Customer balance cannot exist when using in-app purchases."); |
|
} |
|
|
|
if (!inAppPurchase && customer != null && stripeCustomerMetadata.ContainsKey("appleReceipt")) |
|
{ |
|
throw new GatewayException("Cannot change from in-app payment method. Contact support."); |
|
} |
|
|
|
var hadBtCustomer = stripeCustomerMetadata.ContainsKey("btCustomerId"); |
|
if (stripePaymentMethod) |
|
{ |
|
if (paymentToken.StartsWith("pm_")) |
|
{ |
|
stipeCustomerPaymentMethodId = paymentToken; |
|
} |
|
else |
|
{ |
|
stipeCustomerSourceToken = paymentToken; |
|
} |
|
} |
|
else if (paymentMethodType == PaymentMethodType.PayPal) |
|
{ |
|
if (hadBtCustomer) |
|
{ |
|
var pmResult = await _btGateway.PaymentMethod.CreateAsync(new Braintree.PaymentMethodRequest |
|
{ |
|
CustomerId = stripeCustomerMetadata["btCustomerId"], |
|
PaymentMethodNonce = paymentToken |
|
}); |
|
|
|
if (pmResult.IsSuccess()) |
|
{ |
|
var customerResult = await _btGateway.Customer.UpdateAsync( |
|
stripeCustomerMetadata["btCustomerId"], new Braintree.CustomerRequest |
|
{ |
|
DefaultPaymentMethodToken = pmResult.Target.Token |
|
}); |
|
|
|
if (customerResult.IsSuccess() && customerResult.Target.PaymentMethods.Length > 0) |
|
{ |
|
braintreeCustomer = customerResult.Target; |
|
} |
|
else |
|
{ |
|
await _btGateway.PaymentMethod.DeleteAsync(pmResult.Target.Token); |
|
hadBtCustomer = false; |
|
} |
|
} |
|
else |
|
{ |
|
hadBtCustomer = false; |
|
} |
|
} |
|
|
|
if (!hadBtCustomer) |
|
{ |
|
var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest |
|
{ |
|
PaymentMethodNonce = paymentToken, |
|
Email = subscriber.BillingEmailAddress(), |
|
Id = subscriber.BraintreeCustomerIdPrefix() + subscriber.Id.ToString("N").ToLower() + |
|
Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false), |
|
CustomFields = new Dictionary<string, string> |
|
{ |
|
[subscriber.BraintreeIdField()] = subscriber.Id.ToString(), |
|
[subscriber.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion |
|
} |
|
}); |
|
|
|
if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0) |
|
{ |
|
throw new GatewayException("Failed to create PayPal customer record."); |
|
} |
|
|
|
braintreeCustomer = customerResult.Target; |
|
} |
|
} |
|
else if (paymentMethodType == PaymentMethodType.AppleInApp) |
|
{ |
|
appleReceiptStatus = await _appleIapService.GetVerifiedReceiptStatusAsync(paymentToken); |
|
if (appleReceiptStatus == null) |
|
{ |
|
throw new GatewayException("Cannot verify Apple in-app purchase."); |
|
} |
|
await VerifyAppleReceiptNotInUseAsync(appleReceiptStatus.GetOriginalTransactionId(), subscriber); |
|
} |
|
else |
|
{ |
|
throw new GatewayException("Payment method is not supported at this time."); |
|
} |
|
|
|
if (stripeCustomerMetadata.ContainsKey("btCustomerId")) |
|
{ |
|
if (braintreeCustomer?.Id != stripeCustomerMetadata["btCustomerId"]) |
|
{ |
|
var nowSec = Utilities.CoreHelpers.ToEpocSeconds(DateTime.UtcNow); |
|
stripeCustomerMetadata.Add($"btCustomerId_{nowSec}", stripeCustomerMetadata["btCustomerId"]); |
|
} |
|
stripeCustomerMetadata["btCustomerId"] = braintreeCustomer?.Id; |
|
} |
|
else if (!string.IsNullOrWhiteSpace(braintreeCustomer?.Id)) |
|
{ |
|
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id); |
|
} |
|
|
|
if (appleReceiptStatus != null) |
|
{ |
|
var originalTransactionId = appleReceiptStatus.GetOriginalTransactionId(); |
|
if (stripeCustomerMetadata.ContainsKey("appleReceipt")) |
|
{ |
|
if (originalTransactionId != stripeCustomerMetadata["appleReceipt"]) |
|
{ |
|
var nowSec = Utilities.CoreHelpers.ToEpocSeconds(DateTime.UtcNow); |
|
stripeCustomerMetadata.Add($"appleReceipt_{nowSec}", stripeCustomerMetadata["appleReceipt"]); |
|
} |
|
stripeCustomerMetadata["appleReceipt"] = originalTransactionId; |
|
} |
|
else |
|
{ |
|
stripeCustomerMetadata.Add("appleReceipt", originalTransactionId); |
|
} |
|
await _appleIapService.SaveReceiptAsync(appleReceiptStatus, subscriber.Id); |
|
} |
|
|
|
try |
|
{ |
|
if (customer == null) |
|
{ |
|
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions |
|
{ |
|
Description = subscriber.BillingName(), |
|
Email = subscriber.BillingEmailAddress(), |
|
Metadata = stripeCustomerMetadata, |
|
Source = stipeCustomerSourceToken, |
|
PaymentMethod = stipeCustomerPaymentMethodId, |
|
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions |
|
{ |
|
DefaultPaymentMethod = stipeCustomerPaymentMethodId, |
|
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions> |
|
{ |
|
new Stripe.CustomerInvoiceSettingsCustomFieldOptions() |
|
{ |
|
Name = subscriber.SubscriberType(), |
|
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()), |
|
}, |
|
} |
|
}, |
|
Address = taxInfo == null ? null : new Stripe.AddressOptions |
|
{ |
|
Country = taxInfo.BillingAddressCountry, |
|
PostalCode = taxInfo.BillingAddressPostalCode, |
|
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty, |
|
Line2 = taxInfo.BillingAddressLine2, |
|
City = taxInfo.BillingAddressCity, |
|
State = taxInfo.BillingAddressState, |
|
}, |
|
Expand = new List<string> { "sources" }, |
|
}); |
|
|
|
subscriber.Gateway = GatewayType.Stripe; |
|
subscriber.GatewayCustomerId = customer.Id; |
|
createdCustomer = true; |
|
} |
|
|
|
if (!createdCustomer) |
|
{ |
|
string defaultSourceId = null; |
|
string defaultPaymentMethodId = null; |
|
if (stripePaymentMethod) |
|
{ |
|
if (!string.IsNullOrWhiteSpace(stipeCustomerSourceToken) && paymentToken.StartsWith("btok_")) |
|
{ |
|
var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new Stripe.BankAccountCreateOptions |
|
{ |
|
Source = paymentToken |
|
}); |
|
defaultSourceId = bankAccount.Id; |
|
} |
|
else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId)) |
|
{ |
|
await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId, |
|
new Stripe.PaymentMethodAttachOptions { Customer = customer.Id }); |
|
defaultPaymentMethodId = stipeCustomerPaymentMethodId; |
|
} |
|
} |
|
|
|
if (customer.Sources != null) |
|
{ |
|
foreach (var source in customer.Sources.Where(s => s.Id != defaultSourceId)) |
|
{ |
|
if (source is Stripe.BankAccount) |
|
{ |
|
await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id); |
|
} |
|
else if (source is Stripe.Card) |
|
{ |
|
await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id); |
|
} |
|
} |
|
} |
|
|
|
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new Stripe.PaymentMethodListOptions |
|
{ |
|
Customer = customer.Id, |
|
Type = "card" |
|
}); |
|
foreach (var cardMethod in cardPaymentMethods.Where(m => m.Id != defaultPaymentMethodId)) |
|
{ |
|
await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new Stripe.PaymentMethodDetachOptions()); |
|
} |
|
|
|
customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions |
|
{ |
|
Metadata = stripeCustomerMetadata, |
|
DefaultSource = defaultSourceId, |
|
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions |
|
{ |
|
DefaultPaymentMethod = defaultPaymentMethodId, |
|
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions> |
|
{ |
|
new Stripe.CustomerInvoiceSettingsCustomFieldOptions() |
|
{ |
|
Name = subscriber.SubscriberType(), |
|
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()) |
|
}, |
|
} |
|
}, |
|
Address = taxInfo == null ? null : new Stripe.AddressOptions |
|
{ |
|
Country = taxInfo.BillingAddressCountry, |
|
PostalCode = taxInfo.BillingAddressPostalCode, |
|
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty, |
|
Line2 = taxInfo.BillingAddressLine2, |
|
City = taxInfo.BillingAddressCity, |
|
State = taxInfo.BillingAddressState, |
|
}, |
|
}); |
|
} |
|
} |
|
catch |
|
{ |
|
if (braintreeCustomer != null && !hadBtCustomer) |
|
{ |
|
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); |
|
} |
|
throw; |
|
} |
|
|
|
return createdCustomer; |
|
} |
|
|
|
public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount) |
|
{ |
|
Stripe.Customer customer = null; |
|
var customerExists = subscriber.Gateway == GatewayType.Stripe && |
|
!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId); |
|
if (customerExists) |
|
{ |
|
customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId); |
|
} |
|
else |
|
{ |
|
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions |
|
{ |
|
Email = subscriber.BillingEmailAddress(), |
|
Description = subscriber.BillingName(), |
|
}); |
|
subscriber.Gateway = GatewayType.Stripe; |
|
subscriber.GatewayCustomerId = customer.Id; |
|
} |
|
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions |
|
{ |
|
Balance = customer.Balance - (long)(creditAmount * 100) |
|
}); |
|
return !customerExists; |
|
} |
|
|
|
public async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber) |
|
{ |
|
var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions()); |
|
var billingInfo = new BillingInfo |
|
{ |
|
Balance = GetBillingBalance(customer), |
|
PaymentSource = await GetBillingPaymentSourceAsync(customer), |
|
Invoices = await GetBillingInvoicesAsync(customer), |
|
Transactions = await GetBillingTransactionsAsync(subscriber) |
|
}; |
|
|
|
return billingInfo; |
|
} |
|
|
|
public async Task<BillingInfo> GetBillingBalanceAndSourceAsync(ISubscriber subscriber) |
|
{ |
|
var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions()); |
|
var billingInfo = new BillingInfo |
|
{ |
|
Balance = GetBillingBalance(customer), |
|
PaymentSource = await GetBillingPaymentSourceAsync(customer) |
|
}; |
|
|
|
return billingInfo; |
|
} |
|
|
|
public async Task<BillingInfo> GetBillingHistoryAsync(ISubscriber subscriber) |
|
{ |
|
var customer = await GetCustomerAsync(subscriber.GatewayCustomerId); |
|
var billingInfo = new BillingInfo |
|
{ |
|
Transactions = await GetBillingTransactionsAsync(subscriber), |
|
Invoices = await GetBillingInvoicesAsync(customer) |
|
}; |
|
|
|
return billingInfo; |
|
} |
|
|
|
public async Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber) |
|
{ |
|
var subscriptionInfo = new SubscriptionInfo(); |
|
|
|
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) |
|
{ |
|
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId); |
|
|
|
if (customer.Discount != null) |
|
{ |
|
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(customer.Discount); |
|
} |
|
|
|
if (subscriber.IsUser()) |
|
{ |
|
subscriptionInfo.UsingInAppPurchase = customer.Metadata.ContainsKey("appleReceipt"); |
|
} |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) |
|
{ |
|
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId); |
|
if (sub != null) |
|
{ |
|
subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub); |
|
} |
|
|
|
if (!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) |
|
{ |
|
try |
|
{ |
|
var upcomingInvoice = await _stripeAdapter.InvoiceUpcomingAsync( |
|
new Stripe.UpcomingInvoiceOptions { Customer = subscriber.GatewayCustomerId }); |
|
if (upcomingInvoice != null) |
|
{ |
|
subscriptionInfo.UpcomingInvoice = |
|
new SubscriptionInfo.BillingUpcomingInvoice(upcomingInvoice); |
|
} |
|
} |
|
catch (Stripe.StripeException) { } |
|
} |
|
} |
|
|
|
return subscriptionInfo; |
|
} |
|
|
|
public async Task<TaxInfo> GetTaxInfoAsync(ISubscriber subscriber) |
|
{ |
|
if (subscriber == null || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) |
|
{ |
|
return null; |
|
} |
|
|
|
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, |
|
new Stripe.CustomerGetOptions { Expand = new List<string> { "tax_ids" } }); |
|
|
|
if (customer == null) |
|
{ |
|
return null; |
|
} |
|
|
|
var address = customer.Address; |
|
var taxId = customer.TaxIds?.FirstOrDefault(); |
|
|
|
// Line1 is required, so if missing we're using the subscriber name |
|
// see: https://stripe.com/docs/api/customers/create#create_customer-address-line1 |
|
if (address != null && string.IsNullOrWhiteSpace(address.Line1)) |
|
{ |
|
address.Line1 = null; |
|
} |
|
|
|
return new TaxInfo |
|
{ |
|
TaxIdNumber = taxId?.Value, |
|
BillingAddressLine1 = address?.Line1, |
|
BillingAddressLine2 = address?.Line2, |
|
BillingAddressCity = address?.City, |
|
BillingAddressState = address?.State, |
|
BillingAddressPostalCode = address?.PostalCode, |
|
BillingAddressCountry = address?.Country, |
|
}; |
|
} |
|
|
|
public async Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo) |
|
{ |
|
if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) |
|
{ |
|
var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new Stripe.CustomerUpdateOptions |
|
{ |
|
Address = new Stripe.AddressOptions |
|
{ |
|
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty, |
|
Line2 = taxInfo.BillingAddressLine2, |
|
City = taxInfo.BillingAddressCity, |
|
State = taxInfo.BillingAddressState, |
|
PostalCode = taxInfo.BillingAddressPostalCode, |
|
Country = taxInfo.BillingAddressCountry, |
|
}, |
|
Expand = new List<string> { "tax_ids" } |
|
}); |
|
|
|
if (!subscriber.IsUser() && customer != null) |
|
{ |
|
var taxId = customer.TaxIds?.FirstOrDefault(); |
|
|
|
if (taxId != null) |
|
{ |
|
await _stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id); |
|
} |
|
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) && |
|
!string.IsNullOrWhiteSpace(taxInfo.TaxIdType)) |
|
{ |
|
await _stripeAdapter.TaxIdCreateAsync(customer.Id, new Stripe.TaxIdCreateOptions |
|
{ |
|
Type = taxInfo.TaxIdType, |
|
Value = taxInfo.TaxIdNumber, |
|
}); |
|
} |
|
} |
|
} |
|
} |
|
|
|
public async Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate) |
|
{ |
|
var stripeTaxRateOptions = new Stripe.TaxRateCreateOptions() |
|
{ |
|
DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}", |
|
Inclusive = false, |
|
Percentage = taxRate.Rate, |
|
Active = true |
|
}; |
|
var stripeTaxRate = await _stripeAdapter.TaxRateCreateAsync(stripeTaxRateOptions); |
|
taxRate.Id = stripeTaxRate.Id; |
|
await _taxRateRepository.CreateAsync(taxRate); |
|
return taxRate; |
|
} |
|
|
|
public async Task UpdateTaxRateAsync(TaxRate taxRate) |
|
{ |
|
if (string.IsNullOrWhiteSpace(taxRate.Id)) |
|
{ |
|
return; |
|
} |
|
|
|
await ArchiveTaxRateAsync(taxRate); |
|
await CreateTaxRateAsync(taxRate); |
|
} |
|
|
|
public async Task ArchiveTaxRateAsync(TaxRate taxRate) |
|
{ |
|
if (string.IsNullOrWhiteSpace(taxRate.Id)) |
|
{ |
|
return; |
|
} |
|
|
|
var updatedStripeTaxRate = await _stripeAdapter.TaxRateUpdateAsync( |
|
taxRate.Id, |
|
new Stripe.TaxRateUpdateOptions() { Active = false } |
|
); |
|
if (!updatedStripeTaxRate.Active) |
|
{ |
|
taxRate.Active = false; |
|
await _taxRateRepository.ArchiveAsync(taxRate); |
|
} |
|
} |
|
|
|
public async Task<string> AddSecretsManagerToSubscription(Organization org, StaticStore.Plan plan, int additionalSmSeats, |
|
int additionalServiceAccount, DateTime? prorationDate = null) |
|
{ |
|
return await FinalizeSubscriptionChangeAsync(org, new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount), prorationDate); |
|
} |
|
|
|
private Stripe.PaymentMethod GetLatestCardPaymentMethod(string customerId) |
|
{ |
|
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging( |
|
new Stripe.PaymentMethodListOptions { Customer = customerId, Type = "card" }); |
|
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault(); |
|
} |
|
|
|
private async Task VerifyAppleReceiptNotInUseAsync(string receiptOriginalTransactionId, ISubscriber subscriber) |
|
{ |
|
var existingReceipt = await _appleIapService.GetReceiptAsync(receiptOriginalTransactionId); |
|
if (existingReceipt != null && existingReceipt.Item2.HasValue && existingReceipt.Item2 != subscriber.Id) |
|
{ |
|
var existingUser = await _userRepository.GetByIdAsync(existingReceipt.Item2.Value); |
|
if (existingUser != null) |
|
{ |
|
throw new GatewayException("Apple receipt already in use by another user."); |
|
} |
|
} |
|
} |
|
|
|
private decimal GetBillingBalance(Stripe.Customer customer) |
|
{ |
|
return customer != null ? customer.Balance / 100M : default; |
|
} |
|
|
|
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Stripe.Customer customer) |
|
{ |
|
if (customer == null) |
|
{ |
|
return null; |
|
} |
|
|
|
if (customer.Metadata?.ContainsKey("appleReceipt") ?? false) |
|
{ |
|
return new BillingInfo.BillingSource |
|
{ |
|
Type = PaymentMethodType.AppleInApp |
|
}; |
|
} |
|
|
|
if (customer.Metadata?.ContainsKey("btCustomerId") ?? false) |
|
{ |
|
try |
|
{ |
|
var braintreeCustomer = await _btGateway.Customer.FindAsync( |
|
customer.Metadata["btCustomerId"]); |
|
if (braintreeCustomer?.DefaultPaymentMethod != null) |
|
{ |
|
return new BillingInfo.BillingSource( |
|
braintreeCustomer.DefaultPaymentMethod); |
|
} |
|
} |
|
catch (Braintree.Exceptions.NotFoundException) { } |
|
} |
|
|
|
if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card") |
|
{ |
|
return new BillingInfo.BillingSource( |
|
customer.InvoiceSettings.DefaultPaymentMethod); |
|
} |
|
|
|
if (customer.DefaultSource != null && |
|
(customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount)) |
|
{ |
|
return new BillingInfo.BillingSource(customer.DefaultSource); |
|
} |
|
|
|
var paymentMethod = GetLatestCardPaymentMethod(customer.Id); |
|
return paymentMethod != null ? new BillingInfo.BillingSource(paymentMethod) : null; |
|
} |
|
|
|
private Stripe.CustomerGetOptions GetCustomerPaymentOptions() |
|
{ |
|
var customerOptions = new Stripe.CustomerGetOptions(); |
|
customerOptions.AddExpand("default_source"); |
|
customerOptions.AddExpand("invoice_settings.default_payment_method"); |
|
return customerOptions; |
|
} |
|
|
|
private async Task<Stripe.Customer> GetCustomerAsync(string gatewayCustomerId, Stripe.CustomerGetOptions options = null) |
|
{ |
|
if (string.IsNullOrWhiteSpace(gatewayCustomerId)) |
|
{ |
|
return null; |
|
} |
|
|
|
Stripe.Customer customer = null; |
|
try |
|
{ |
|
customer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId, options); |
|
} |
|
catch (Stripe.StripeException) { } |
|
|
|
return customer; |
|
} |
|
|
|
private async Task<IEnumerable<BillingInfo.BillingTransaction>> GetBillingTransactionsAsync(ISubscriber subscriber) |
|
{ |
|
ICollection<Transaction> transactions = null; |
|
if (subscriber is User) |
|
{ |
|
transactions = await _transactionRepository.GetManyByUserIdAsync(subscriber.Id); |
|
} |
|
else if (subscriber is Organization) |
|
{ |
|
transactions = await _transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id); |
|
} |
|
|
|
return transactions?.OrderByDescending(i => i.CreationDate) |
|
.Select(t => new BillingInfo.BillingTransaction(t)); |
|
|
|
} |
|
|
|
private async Task<IEnumerable<BillingInfo.BillingInvoice>> GetBillingInvoicesAsync(Stripe.Customer customer) |
|
{ |
|
if (customer == null) |
|
{ |
|
return null; |
|
} |
|
|
|
var options = new StripeInvoiceListOptions |
|
{ |
|
Customer = customer.Id, |
|
SelectAll = true |
|
}; |
|
|
|
try |
|
{ |
|
var invoices = await _stripeAdapter.InvoiceListAsync(options); |
|
|
|
return invoices |
|
.Where(invoice => invoice.Status != "void" && invoice.Status != "draft") |
|
.OrderByDescending(invoice => invoice.Created) |
|
.Select(invoice => new BillingInfo.BillingInvoice(invoice)); |
|
} |
|
catch (Stripe.StripeException exception) |
|
{ |
|
_logger.LogError(exception, "An error occurred while listing Stripe invoices"); |
|
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 |
|
public static string GetFirstThirtyCharacters(string subscriberName) |
|
{ |
|
if (string.IsNullOrWhiteSpace(subscriberName)) |
|
{ |
|
return ""; |
|
} |
|
else if (subscriberName.Length <= 30) |
|
{ |
|
return subscriberName; |
|
} |
|
else |
|
{ |
|
return subscriberName.Substring(0, 30); |
|
} |
|
} |
|
}
|
|
|