From 952d624d7219ed1c26cab0b571752294ac9dded2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 31 Jan 2019 12:11:30 -0500 Subject: [PATCH] change payment methods between stripe and paypal --- src/Core/Models/Table/ISubscriber.cs | 5 +- src/Core/Models/Table/Organization.cs | 5 + src/Core/Models/Table/User.cs | 5 + src/Core/Services/IPaymentService.cs | 5 +- .../BraintreePaymentService.cs | 12 +- .../Implementations/OrganizationService.cs | 19 +- .../Implementations/StripePaymentService.cs | 164 +++++++++++++----- .../Services/Implementations/UserService.cs | 9 +- 8 files changed, 172 insertions(+), 52 deletions(-) diff --git a/src/Core/Models/Table/ISubscriber.cs b/src/Core/Models/Table/ISubscriber.cs index 84f2a6aff5..9b2ffdbdbc 100644 --- a/src/Core/Models/Table/ISubscriber.cs +++ b/src/Core/Models/Table/ISubscriber.cs @@ -1,15 +1,18 @@ -using Bit.Core.Enums; +using System; +using Bit.Core.Enums; using Bit.Core.Services; namespace Bit.Core.Models.Table { public interface ISubscriber { + Guid Id { get; } GatewayType? Gateway { get; set; } string GatewayCustomerId { get; set; } string GatewaySubscriptionId { get; set; } string BillingEmailAddress(); string BillingName(); + string BraintreeCustomerIdPrefix(); IPaymentService GetPaymentService(GlobalSettings globalSettings); } } diff --git a/src/Core/Models/Table/Organization.cs b/src/Core/Models/Table/Organization.cs index be7722bdf0..09d56ac4a1 100644 --- a/src/Core/Models/Table/Organization.cs +++ b/src/Core/Models/Table/Organization.cs @@ -63,6 +63,11 @@ namespace Bit.Core.Models.Table return BusinessName; } + public string BraintreeCustomerIdPrefix() + { + return "o"; + } + public long StorageBytesRemaining() { if(!MaxStorageGb.HasValue) diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs index d7c7d72099..2850254c06 100644 --- a/src/Core/Models/Table/User.cs +++ b/src/Core/Models/Table/User.cs @@ -58,6 +58,11 @@ namespace Bit.Core.Models.Table return Name; } + public string BraintreeCustomerIdPrefix() + { + return "u"; + } + public Dictionary GetTwoFactorProviders() { if(string.IsNullOrWhiteSpace(TwoFactorProviders)) diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 8d23ddd410..d45e378b39 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -8,12 +8,13 @@ namespace Bit.Core.Services public interface IPaymentService { Task CancelAndRecoverChargesAsync(ISubscriber subscriber); - Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, + Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, short additionalStorageGb); Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId); Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false); Task ReinstateSubscriptionAsync(ISubscriber subscriber); - Task UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken); + Task UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, + string paymentToken); Task GetUpcomingInvoiceAsync(ISubscriber subscriber); Task GetBillingAsync(ISubscriber subscriber); } diff --git a/src/Core/Services/Implementations/BraintreePaymentService.cs b/src/Core/Services/Implementations/BraintreePaymentService.cs index eb8a132704..8c1a805ab3 100644 --- a/src/Core/Services/Implementations/BraintreePaymentService.cs +++ b/src/Core/Services/Implementations/BraintreePaymentService.cs @@ -215,7 +215,7 @@ namespace Bit.Core.Services return billingInfo; } - public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, + public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, short additionalStorageGb) { var customerResult = await _gateway.Customer.CreateAsync(new CustomerRequest @@ -303,14 +303,20 @@ namespace Bit.Core.Services } } - public async Task UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken) + public async Task UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, + string paymentToken) { if(subscriber == null) { throw new ArgumentNullException(nameof(subscriber)); } - if(subscriber.Gateway.HasValue && subscriber.Gateway.Value != Enums.GatewayType.Braintree) + if(paymentMethodType != PaymentMethodType.PayPal) + { + throw new GatewayException("Payment method not allowed"); + } + + if(subscriber.Gateway.HasValue && subscriber.Gateway.Value != GatewayType.Braintree) { throw new GatewayException("Switching from one payment type to another is not supported. " + "Contact us for assistance."); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 2af6bdefc4..067810c22f 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -78,7 +78,22 @@ namespace Bit.Core.Services throw new NotFoundException(); } - var updated = await _stripePaymentService.UpdatePaymentMethodAsync(organization, paymentToken); + PaymentMethodType paymentMethodType; + if(paymentToken.StartsWith("btok_")) + { + paymentMethodType = PaymentMethodType.BankAccount; + } + else if(paymentToken.StartsWith("tok_")) + { + paymentMethodType = PaymentMethodType.Card; + } + else + { + paymentMethodType = PaymentMethodType.PayPal; + } + + var updated = await _stripePaymentService.UpdatePaymentMethodAsync(organization, + paymentMethodType, paymentToken); if(updated) { await ReplaceAndUpdateCache(organization); @@ -340,7 +355,7 @@ namespace Bit.Core.Services { throw new BadRequestException("Subscription not found."); } - + Func> subUpdateAction = null; var seatItem = sub.Items?.Data?.FirstOrDefault(i => i.Plan.Id == plan.StripeSeatPlanId); var subItemOptions = sub.Items.Where(i => i.Plan.Id != plan.StripeSeatPlanId) diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 9109f91f1f..3a23e857df 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -65,6 +65,10 @@ namespace Bit.Core.Services braintreeCustomer = customerResult.Target; stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id); } + else + { + throw new GatewayException("Payment method is not supported at this time."); + } var customer = await customerService.CreateAsync(new CustomerCreateOptions { @@ -542,20 +546,26 @@ namespace Bit.Core.Services } } - public async Task UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken) + public async Task UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, + string paymentToken) { if(subscriber == null) { throw new ArgumentNullException(nameof(subscriber)); } - if(subscriber.Gateway.HasValue && subscriber.Gateway.Value != Enums.GatewayType.Stripe) + 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 updatedSubscriber = false; + var createdCustomer = false; + Braintree.Customer braintreeCustomer = null; + string stipeCustomerSourceToken = null; + var stripeCustomerMetadata = new Dictionary(); + var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card || + paymentMethodType == PaymentMethodType.BankAccount; var cardService = new CardService(); var bankSerice = new BankAccountService(); @@ -565,53 +575,122 @@ namespace Bit.Core.Services if(!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) { customer = await customerService.GetAsync(subscriber.GatewayCustomerId); + if(customer.Metadata?.Any() ?? false) + { + stripeCustomerMetadata = customer.Metadata; + } } - if(customer == null) + if(stripeCustomerMetadata.ContainsKey("btCustomerId")) { - customer = await customerService.CreateAsync(new CustomerCreateOptions + var nowSec = Utilities.CoreHelpers.ToEpocSeconds(DateTime.UtcNow); + stripeCustomerMetadata.Add($"btCustomerId_{nowSec}", stripeCustomerMetadata["btCustomerId"]); + stripeCustomerMetadata["btCustomerId"] = null; + } + + if(stripePaymentMethod) + { + 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 { - Description = subscriber.BillingName(), + PaymentMethodNonce = paymentToken, Email = subscriber.BillingEmailAddress(), - SourceToken = paymentToken + Id = subscriber.BraintreeCustomerIdPrefix() + subscriber.Id.ToString("N").ToLower() + randomSuffix }); - subscriber.Gateway = Enums.GatewayType.Stripe; - subscriber.GatewayCustomerId = customer.Id; - updatedSubscriber = true; - } - else - { - if(paymentToken.StartsWith("btok_")) + if(!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0) { - await bankSerice.CreateAsync(customer.Id, new BankAccountCreateOptions - { - SourceToken = paymentToken - }); + throw new GatewayException("Failed to create PayPal customer record."); + } + + braintreeCustomer = customerResult.Target; + if(stripeCustomerMetadata.ContainsKey("btCustomerId")) + { + stripeCustomerMetadata["btCustomerId"] = braintreeCustomer.Id; } else { - await cardService.CreateAsync(customer.Id, new CardCreateOptions - { - SourceToken = paymentToken - }); - } - - if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId)) - { - var source = customer.Sources.FirstOrDefault(s => s.Id == customer.DefaultSourceId); - if(source is BankAccount) - { - await bankSerice.DeleteAsync(customer.Id, customer.DefaultSourceId); - } - else if(source is Card) - { - await cardService.DeleteAsync(customer.Id, customer.DefaultSourceId); - } + stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id); } } + else + { + throw new GatewayException("Payment method is not supported at this time."); + } - return updatedSubscriber; + try + { + if(customer == null) + { + customer = await customerService.CreateAsync(new CustomerCreateOptions + { + Description = subscriber.BillingName(), + Email = subscriber.BillingEmailAddress(), + SourceToken = stipeCustomerSourceToken, + Metadata = stripeCustomerMetadata + }); + + subscriber.Gateway = GatewayType.Stripe; + subscriber.GatewayCustomerId = customer.Id; + createdCustomer = true; + } + + if(!createdCustomer) + { + string defaultSourceId = null; + if(stripePaymentMethod) + { + if(paymentToken.StartsWith("btok_")) + { + var bankAccount = await bankSerice.CreateAsync(customer.Id, new BankAccountCreateOptions + { + SourceToken = paymentToken + }); + defaultSourceId = bankAccount.Id; + } + else + { + var card = await cardService.CreateAsync(customer.Id, new CardCreateOptions + { + SourceToken = paymentToken, + }); + defaultSourceId = card.Id; + } + } + + foreach(var source in customer.Sources.Where(s => s.Id != defaultSourceId)) + { + if(source is BankAccount) + { + await bankSerice.DeleteAsync(customer.Id, source.Id); + } + else if(source is Card) + { + await cardService.DeleteAsync(customer.Id, source.Id); + } + } + + customer = await customerService.UpdateAsync(customer.Id, new CustomerUpdateOptions + { + Metadata = stripeCustomerMetadata, + DefaultSource = defaultSourceId + }); + } + } + catch(Exception e) + { + if(braintreeCustomer != null) + { + await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); + } + throw e; + } + + return createdCustomer; } public async Task GetUpcomingInvoiceAsync(ISubscriber subscriber) @@ -660,12 +739,17 @@ namespace Bit.Core.Services if(customer.Metadata?.ContainsKey("btCustomerId") ?? false) { - var braintreeCustomer = await _btGateway.Customer.FindAsync(customer.Metadata["btCustomerId"]); - if(braintreeCustomer?.DefaultPaymentMethod != null) + try { - billingInfo.PaymentSource = new BillingInfo.BillingSource( - braintreeCustomer.DefaultPaymentMethod); + var braintreeCustomer = await _btGateway.Customer.FindAsync( + customer.Metadata["btCustomerId"]); + if(braintreeCustomer?.DefaultPaymentMethod != null) + { + billingInfo.PaymentSource = new BillingInfo.BillingSource( + braintreeCustomer.DefaultPaymentMethod); + } } + catch(Braintree.Exceptions.NotFoundException) { } } else if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId) && customer.Sources?.Data != null) { diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 03553401f1..af15171f7f 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -800,17 +800,18 @@ namespace Bit.Core.Services throw new BadRequestException("Invalid token."); } - IPaymentService paymentService = null; + PaymentMethodType paymentMethodType; + var paymentService = new StripePaymentService(_globalSettings); if(paymentToken.StartsWith("tok_")) { - paymentService = new StripePaymentService(_globalSettings); + paymentMethodType = PaymentMethodType.Card; } else { - paymentService = new BraintreePaymentService(_globalSettings); + paymentMethodType = PaymentMethodType.PayPal; } - var updated = await paymentService.UpdatePaymentMethodAsync(user, paymentToken); + var updated = await paymentService.UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken); if(updated) { await SaveUserAsync(user);