From e54a381dba79826de3ce602099ec84547236e43e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 29 Jan 2019 13:12:11 -0500 Subject: [PATCH] setup: process paypal with stripe subscription --- .../Controllers/OrganizationsController.cs | 2 +- src/Core/Models/Table/Organization.cs | 2 +- src/Core/Models/Table/User.cs | 2 +- src/Core/Services/IPaymentService.cs | 4 +- .../BraintreePaymentService.cs | 4 +- .../Implementations/OrganizationService.cs | 4 +- .../Implementations/StripePaymentService.cs | 145 +++++++++++++++++- .../Services/Implementations/UserService.cs | 14 +- 8 files changed, 157 insertions(+), 20 deletions(-) diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 340b07908..5ab83bc30 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -78,7 +78,7 @@ namespace Bit.Api.Controllers if(!_globalSettings.SelfHosted && organization.Gateway != null) { - var paymentService = new StripePaymentService(); + var paymentService = new StripePaymentService(_globalSettings); var billingInfo = await paymentService.GetBillingAsync(organization); if(billingInfo == null) { diff --git a/src/Core/Models/Table/Organization.cs b/src/Core/Models/Table/Organization.cs index f398d3908..be7722bdf 100644 --- a/src/Core/Models/Table/Organization.cs +++ b/src/Core/Models/Table/Organization.cs @@ -95,7 +95,7 @@ namespace Bit.Core.Models.Table switch(Gateway) { case GatewayType.Stripe: - paymentService = new StripePaymentService(); + paymentService = new StripePaymentService(globalSettings); break; case GatewayType.Braintree: paymentService = new BraintreePaymentService(globalSettings); diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs index ce0a73319..d7c7d7209 100644 --- a/src/Core/Models/Table/User.cs +++ b/src/Core/Models/Table/User.cs @@ -144,7 +144,7 @@ namespace Bit.Core.Models.Table switch(Gateway) { case GatewayType.Stripe: - paymentService = new StripePaymentService(); + paymentService = new StripePaymentService(globalSettings); break; case GatewayType.Braintree: paymentService = new BraintreePaymentService(globalSettings); diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 41da719bf..8d23ddd41 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -1,13 +1,15 @@ using System.Threading.Tasks; using Bit.Core.Models.Table; using Bit.Core.Models.Business; +using Bit.Core.Enums; namespace Bit.Core.Services { public interface IPaymentService { Task CancelAndRecoverChargesAsync(ISubscriber subscriber); - Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb); + 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); diff --git a/src/Core/Services/Implementations/BraintreePaymentService.cs b/src/Core/Services/Implementations/BraintreePaymentService.cs index 43ec65ae0..eb8a13270 100644 --- a/src/Core/Services/Implementations/BraintreePaymentService.cs +++ b/src/Core/Services/Implementations/BraintreePaymentService.cs @@ -5,6 +5,7 @@ using Bit.Core.Models.Table; using Braintree; using Bit.Core.Exceptions; using Bit.Core.Models.Business; +using Bit.Core.Enums; namespace Bit.Core.Services { @@ -214,7 +215,8 @@ namespace Bit.Core.Services return billingInfo; } - public async Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb) + public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, + short additionalStorageGb) { var customerResult = await _gateway.Customer.CreateAsync(new CustomerRequest { diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 98e886907..1388ae2b2 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -66,7 +66,7 @@ namespace Bit.Core.Services _eventService = eventService; _installationRepository = installationRepository; _applicationCacheService = applicationCacheService; - _stripePaymentService = new StripePaymentService(); + _stripePaymentService = new StripePaymentService(globalSettings); _globalSettings = globalSettings; } @@ -1208,7 +1208,7 @@ namespace Bit.Core.Services throw new BadRequestException("Invalid installation id"); } - var paymentService = new StripePaymentService(); + var paymentService = new StripePaymentService(_globalSettings); var billingInfo = await paymentService.GetBillingAsync(organization); return new OrganizationLicense(organization, billingInfo, installationId, _licensingService); } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 6b37a7bc3..1f34da423 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using Bit.Core.Exceptions; using System.Linq; using Bit.Core.Models.Business; +using Braintree; +using Bit.Core.Enums; namespace Bit.Core.Services { @@ -13,21 +15,68 @@ namespace Bit.Core.Services { private const string PremiumPlanId = "premium-annually"; private const string StoragePlanId = "storage-gb-annually"; + private readonly BraintreeGateway _btGateway; - public async Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb) + public StripePaymentService( + GlobalSettings globalSettings) { + _btGateway = new BraintreeGateway + { + Environment = globalSettings.Braintree.Production ? + Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX, + MerchantId = globalSettings.Braintree.MerchantId, + PublicKey = globalSettings.Braintree.PublicKey, + PrivateKey = globalSettings.Braintree.PrivateKey + }; + } + + public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, + short additionalStorageGb) + { + Customer braintreeCustomer = null; + StripeBilling? stripeSubscriptionBilling = null; + string stipeCustomerSourceToken = null; + var stripeCustomerMetadata = new Dictionary(); + + if(paymentMethodType == PaymentMethodType.PayPal) + { + stripeSubscriptionBilling = StripeBilling.SendInvoice; + var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false); + var customerResult = await _btGateway.Customer.CreateAsync(new CustomerRequest + { + PaymentMethodNonce = paymentToken, + Email = user.Email, + Id = "u" + user.Id.ToString("N").ToLower() + randomSuffix + }); + + 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.Card || paymentMethodType == PaymentMethodType.BankAccount) + { + stipeCustomerSourceToken = paymentToken; + } + var customerService = new StripeCustomerService(); var customer = await customerService.CreateAsync(new StripeCustomerCreateOptions { Description = user.Name, Email = user.Email, - SourceToken = paymentToken + SourceToken = stipeCustomerSourceToken, + Metadata = stripeCustomerMetadata }); var subCreateOptions = new StripeSubscriptionCreateOptions { CustomerId = customer.Id, Items = new List(), + Billing = stripeSubscriptionBilling, + DaysUntilDue = stripeSubscriptionBilling != null ? 1 : 0, Metadata = new Dictionary { ["userId"] = user.Id.ToString() @@ -54,14 +103,70 @@ namespace Bit.Core.Services { var subscriptionService = new StripeSubscriptionService(); subscription = await subscriptionService.CreateAsync(subCreateOptions); + + if(stripeSubscriptionBilling == StripeBilling.SendInvoice) + { + var invoiceService = new StripeInvoiceService(); + var invoices = await invoiceService.ListAsync(new StripeInvoiceListOptions + { + SubscriptionId = subscription.Id + }); + + var invoice = invoices?.FirstOrDefault(i => i.AmountDue > 0); + if(invoice == null) + { + throw new GatewayException("Invoice not found."); + } + + if(braintreeCustomer != null) + { + var btInvoiceAmount = (invoice.AmountDue / 100M); + var transactionResult = await _btGateway.Transaction.SaleAsync(new TransactionRequest + { + Amount = btInvoiceAmount, + CustomerId = braintreeCustomer.Id + }); + + if(!transactionResult.IsSuccess() || transactionResult.Target.Amount != btInvoiceAmount) + { + throw new GatewayException("Failed to charge PayPal customer."); + } + + var invoiceItemService = new StripeInvoiceItemService(); + await invoiceItemService.CreateAsync(new StripeInvoiceItemCreateOptions + { + Currency = "USD", + CustomerId = customer.Id, + InvoiceId = invoice.Id, + Amount = -1 * invoice.AmountDue, + Description = $"PayPal Credit, Transaction ID " + + transactionResult.Target.PayPalDetails.AuthorizationId, + Metadata = new Dictionary + { + ["btTransactionId"] = transactionResult.Target.Id, + ["btPayPalTransactionId"] = transactionResult.Target.PayPalDetails.AuthorizationId + } + }); + } + else + { + throw new GatewayException("No payment was able to be collected."); + } + + await invoiceService.PayAsync(invoice.Id, new StripeInvoicePayOptions { }); + } } - catch(StripeException) + catch(Exception e) { await customerService.DeleteAsync(customer.Id); - throw; + if(braintreeCustomer != null) + { + await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); + } + throw e; } - user.Gateway = Enums.GatewayType.Stripe; + user.Gateway = GatewayType.Stripe; user.GatewayCustomerId = customer.Id; user.GatewaySubscriptionId = subscription.Id; user.Premium = true; @@ -169,6 +274,36 @@ namespace Bit.Core.Services if(invoice.AmountDue > 0) { + var customerService = new StripeCustomerService(); + var customer = await customerService.GetAsync(subscriber.GatewayCustomerId); + if(customer != null) + { + if(customer.Metadata.ContainsKey("btCustomerId")) + { + var invoiceAmount = (invoice.AmountDue / 100M); + var transactionResult = await _btGateway.Transaction.SaleAsync(new TransactionRequest + { + Amount = invoiceAmount, + CustomerId = customer.Metadata["btCustomerId"] + }); + + if(!transactionResult.IsSuccess() || transactionResult.Target.Amount != invoiceAmount) + { + await invoiceService.UpdateAsync(invoice.Id, new StripeInvoiceUpdateOptions + { + Closed = true + }); + throw new GatewayException("Failed to charge PayPal customer."); + } + + await customerService.UpdateAsync(customer.Id, new StripeCustomerUpdateOptions + { + AccountBalance = customer.AccountBalance - invoice.AmountDue, + Metadata = customer.Metadata + }); + } + } + await invoiceService.PayAsync(invoice.Id, new StripeInvoicePayOptions()); } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 0250d2927..03553401f 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -706,16 +706,14 @@ namespace Bit.Core.Services throw new BadRequestException("Invalid token."); } - if(paymentToken.StartsWith("tok_")) + var paymentMethodType = PaymentMethodType.Card; + if(!paymentToken.StartsWith("tok_")) { - paymentService = new StripePaymentService(); - } - else - { - paymentService = new BraintreePaymentService(_globalSettings); + paymentMethodType = PaymentMethodType.PayPal; } - await paymentService.PurchasePremiumAsync(user, paymentToken, additionalStorageGb); + await new StripePaymentService(_globalSettings).PurchasePremiumAsync(user, paymentMethodType, + paymentToken, additionalStorageGb); } else { @@ -805,7 +803,7 @@ namespace Bit.Core.Services IPaymentService paymentService = null; if(paymentToken.StartsWith("tok_")) { - paymentService = new StripePaymentService(); + paymentService = new StripePaymentService(_globalSettings); } else {