From 01d324a8b4f91d34c6c22e5ba3000fc6eed994a5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 20 Feb 2019 23:54:27 -0500 Subject: [PATCH] support credit purchases and prorated upgrades --- .../Request/Accounts/PremiumRequestModel.cs | 3 +- .../Implementations/OrganizationService.cs | 2 +- .../Implementations/StripePaymentService.cs | 94 ++++++++++++++----- .../Services/Implementations/UserService.cs | 6 +- 4 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs b/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs index fed15cd101..fcf89672f6 100644 --- a/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs +++ b/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs @@ -22,7 +22,8 @@ namespace Bit.Core.Models.Api public IEnumerable Validate(ValidationContext validationContext) { - if(string.IsNullOrWhiteSpace(PaymentToken) && License == null) + var creditType = PaymentMethodType.HasValue && PaymentMethodType.Value == Enums.PaymentMethodType.Credit; + if(string.IsNullOrWhiteSpace(PaymentToken) && !creditType && License == null) { yield return new ValidationResult("Payment token or license is required."); } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index f25f833b65..007b932d46 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -550,7 +550,7 @@ namespace Bit.Core.Services } else { - if(!signup.PaymentMethodType.HasValue) + if(!signup.PaymentMethodType.HasValue && !string.IsNullOrWhiteSpace(signup.PaymentToken)) { if(signup.PaymentToken.StartsWith("btok_")) { diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 1fee4c3711..71a7d3f7c3 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -161,6 +161,20 @@ namespace Bit.Core.Services public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, short additionalStorageGb) { + 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) + { + throw new GatewayException("Bank account payment method is not supported at this time."); + } + var invoiceService = new InvoiceService(); var customerService = new CustomerService(); @@ -169,28 +183,26 @@ namespace Bit.Core.Services Braintree.Transaction braintreeTransaction = null; Braintree.Customer braintreeCustomer = null; var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card || - paymentMethodType == PaymentMethodType.BankAccount; - - if(paymentMethodType == PaymentMethodType.BankAccount) - { - throw new GatewayException("Bank account payment method is not supported at this time."); - } + paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.Credit; if(user.Gateway == GatewayType.Stripe && !string.IsNullOrWhiteSpace(user.GatewayCustomerId)) { - try + if(!string.IsNullOrWhiteSpace(paymentToken)) { - await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken); - stripeCustomerId = user.GatewayCustomerId; - createdStripeCustomer = false; - } - catch(Exception) - { - stripeCustomerId = null; + try + { + await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken); + } + catch + { + stripeCustomerId = null; + } } + stripeCustomerId = user.GatewayCustomerId; + createdStripeCustomer = false; } - if(string.IsNullOrWhiteSpace(stripeCustomerId)) + if(string.IsNullOrWhiteSpace(stripeCustomerId) && !string.IsNullOrWhiteSpace(paymentToken)) { string stipeCustomerSourceToken = null; var stripeCustomerMetadata = new Dictionary(); @@ -316,6 +328,18 @@ namespace Bit.Core.Services throw new GatewayException("No payment was able to be collected."); } } + else if(paymentMethodType == PaymentMethodType.Credit) + { + var previewInvoice = await invoiceService.UpcomingAsync(new UpcomingInvoiceOptions + { + CustomerId = stripeCustomerId, + SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items) + }); + if(previewInvoice.AmountDue > 0) + { + throw new GatewayException("Your account does not have enough credit available."); + } + } var subscriptionService = new SubscriptionService(); subscription = await subscriptionService.CreateAsync(subCreateOptions); @@ -533,6 +557,20 @@ namespace Bit.Core.Services // Owes more than prorateThreshold on next invoice. // Invoice them and pay now instead of waiting until next billing cycle. + var customerService = new CustomerService(); + customerService.ExpandDefaultSource = true; + var customer = await customerService.GetAsync(subscriber.GatewayCustomerId); + + var invoiceAmountDue = upcomingPreview.StartingBalance + invoiceAmount; + if(invoiceAmountDue > 0 && !customer.Metadata.ContainsKey("btCustomerId")) + { + if(customer.DefaultSource == null || + (!(customer.DefaultSource is Card) && !(customer.DefaultSource is BankAccount))) + { + // throw new BadRequestException("No payment method is available."); + } + } + Invoice invoice = null; var createdInvoiceItems = new List(); Braintree.Transaction braintreeTransaction = null; @@ -565,14 +603,12 @@ namespace Bit.Core.Services }); var invoicePayOptions = new InvoicePayOptions(); - var customerService = new CustomerService(); - var customer = await customerService.GetAsync(subscriber.GatewayCustomerId); - if(customer != null) + if(invoice.AmountDue > 0) { - if(customer.Metadata.ContainsKey("btCustomerId")) + if(customer?.Metadata?.ContainsKey("btCustomerId") ?? false) { invoicePayOptions.PaidOutOfBand = true; - var btInvoiceAmount = (invoiceAmount / 100M); + var btInvoiceAmount = (invoice.AmountDue / 100M); var transactionResult = await _btGateway.Transaction.SaleAsync( new Braintree.TransactionRequest { @@ -610,7 +646,14 @@ namespace Bit.Core.Services } } - await invoiceService.PayAsync(invoice.Id, invoicePayOptions); + try + { + await invoiceService.PayAsync(invoice.Id, invoicePayOptions); + } + catch(StripeException) + { + throw new GatewayException("Unable to pay invoice."); + } } catch(Exception e) { @@ -620,7 +663,14 @@ namespace Bit.Core.Services } if(invoice != null) { - await invoiceService.DeleteAsync(invoice.Id); + await invoiceService.VoidInvoiceAsync(invoice.Id, new InvoiceVoidOptions()); + if(invoice.StartingBalance != 0) + { + await customerService.UpdateAsync(customer.Id, new CustomerUpdateOptions + { + AccountBalance = customer.AccountBalance + }); + } // Restore invoice items that were brought in foreach(var item in pendingInvoiceItems) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 18e9e735e0..031d2581cf 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -708,7 +708,7 @@ namespace Bit.Core.Services Directory.CreateDirectory(dir); File.WriteAllText($"{dir}/{user.Id}.json", JsonConvert.SerializeObject(license, Formatting.Indented)); } - else if(!string.IsNullOrWhiteSpace(paymentToken)) + else { if(!paymentMethodType.HasValue) { @@ -729,10 +729,6 @@ namespace Bit.Core.Services await _paymentService.PurchasePremiumAsync(user, paymentMethodType.Value, paymentToken, additionalStorageGb); } - else - { - throw new InvalidOperationException("License or payment token is required."); - } user.Premium = true; user.RevisionDate = DateTime.UtcNow;