From 2dc9c196c45cff436ddc677e44440263978a3186 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 28 Jul 2017 00:17:31 -0400 Subject: [PATCH] paymentservice with stripe & braintree implem. --- src/Api/settings.Production.json | 3 + src/Api/settings.json | 6 ++ src/Billing/settings.Production.json | 5 +- src/Billing/settings.json | 6 ++ src/Core/Core.csproj | 1 + src/Core/GlobalSettings.cs | 9 +++ src/Core/Services/IPaymentService.cs | 10 +++ .../BraintreePaymentService.cs | 72 +++++++++++++++++++ .../Implementations/StripePaymentService.cs | 70 ++++++++++++++++++ .../Services/Implementations/UserService.cs | 49 +------------ src/Core/Utilities/CoreHelpers.cs | 68 +++++++++++------- src/Identity/settings.Production.json | 3 + src/Identity/settings.json | 6 ++ 13 files changed, 236 insertions(+), 72 deletions(-) create mode 100644 src/Core/Services/IPaymentService.cs create mode 100644 src/Core/Services/Implementations/BraintreePaymentService.cs create mode 100644 src/Core/Services/Implementations/StripePaymentService.cs diff --git a/src/Api/settings.Production.json b/src/Api/settings.Production.json index 08a068c6bd..57d8453827 100644 --- a/src/Api/settings.Production.json +++ b/src/Api/settings.Production.json @@ -3,6 +3,9 @@ "baseVaultUri": "https://vault.bitwarden.com/#", "u2f": { "appId": "https://vault.bitwarden.com/app-id.json" + }, + "braintree": { + "production": true } } } diff --git a/src/Api/settings.json b/src/Api/settings.json index e52d4c0405..cb93d9e9f3 100644 --- a/src/Api/settings.json +++ b/src/Api/settings.json @@ -40,6 +40,12 @@ }, "u2f": { "appId": "https://localhost:4001/app-id.json" + }, + "braintree": { + "production": false, + "merchantId": "SECRET", + "publicKey": "SECRET", + "privateKey": "SECRET" } }, "IpRateLimitOptions": { diff --git a/src/Billing/settings.Production.json b/src/Billing/settings.Production.json index 8cb3e0000a..fe522cd4f5 100644 --- a/src/Billing/settings.Production.json +++ b/src/Billing/settings.Production.json @@ -1,5 +1,8 @@ { "globalSettings": { - "baseVaultUri": "https://vault.bitwarden.com/#" + "baseVaultUri": "https://vault.bitwarden.com/#", + "braintree": { + "production": true + } } } diff --git a/src/Billing/settings.json b/src/Billing/settings.json index bd6d187ee1..6b7af457a4 100644 --- a/src/Billing/settings.json +++ b/src/Billing/settings.json @@ -30,5 +30,11 @@ }, "billingSettings": { "stripeWebhookKey": "SECRET" + }, + "braintree": { + "production": false, + "merchantId": "SECRET", + "publicKey": "SECRET", + "privateKey": "SECRET" } } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 91161ecaca..caf4e69e23 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Core/GlobalSettings.cs b/src/Core/GlobalSettings.cs index 0a11540be7..1e761c83ec 100644 --- a/src/Core/GlobalSettings.cs +++ b/src/Core/GlobalSettings.cs @@ -16,6 +16,7 @@ public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings(); public virtual DuoSettings Duo { get; set; } = new DuoSettings(); public virtual U2fSettings U2f { get; set; } = new U2fSettings(); + public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings(); public class SqlServerSettings { @@ -86,5 +87,13 @@ { public string AppId { get; set; } } + + public class BraintreeSettings + { + public bool Production { get; set; } + public string MerchantId { get; set; } + public string PublicKey { get; set; } + public string PrivateKey { get; set; } + } } } diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs new file mode 100644 index 0000000000..1e6f66551c --- /dev/null +++ b/src/Core/Services/IPaymentService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Bit.Core.Models.Table; + +namespace Bit.Core.Services +{ + public interface IPaymentService + { + Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb); + } +} diff --git a/src/Core/Services/Implementations/BraintreePaymentService.cs b/src/Core/Services/Implementations/BraintreePaymentService.cs new file mode 100644 index 0000000000..549e9f0aa0 --- /dev/null +++ b/src/Core/Services/Implementations/BraintreePaymentService.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Models.Table; +using Braintree; + +namespace Bit.Core.Services +{ + public class BraintreePaymentService : IPaymentService + { + private readonly BraintreeGateway _gateway; + + public BraintreePaymentService( + GlobalSettings globalSettings) + { + _gateway = 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, string paymentToken, short additionalStorageGb) + { + var customerResult = await _gateway.Customer.CreateAsync(new CustomerRequest + { + PaymentMethodNonce = paymentToken, + Email = user.Email + }); + + if(!customerResult.IsSuccess()) + { + // error, throw something + } + + var subId = "u" + user.Id.ToString("N").ToLower() + + Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false); + + var subRequest = new SubscriptionRequest + { + Id = subId, + PaymentMethodToken = paymentToken, + PlanId = "premium-annually" + }; + + if(additionalStorageGb > 0) + { + subRequest.AddOns.Add = new AddAddOnRequest[] + { + new AddAddOnRequest + { + InheritedFromId = "storage-gb-annually", + Quantity = additionalStorageGb + } + }; + } + + var subResult = await _gateway.Subscription.CreateAsync(subRequest); + + if(!subResult.IsSuccess()) + { + await _gateway.Customer.DeleteAsync(customerResult.Target.Id); + // error, throw something + } + + user.StripeCustomerId = customerResult.Target.Id; + user.StripeSubscriptionId = subResult.Target.Id; + } + } +} diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs new file mode 100644 index 0000000000..aa026e2b57 --- /dev/null +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Models.Table; +using Stripe; +using System.Collections.Generic; + +namespace Bit.Core.Services +{ + public class StripePaymentService : IPaymentService + { + private const string PremiumPlanId = "premium-annually"; + private const string StoragePlanId = "storage-gb-annually"; + + public StripePaymentService( + GlobalSettings globalSettings) + { + + } + + public async Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb) + { + var customerService = new StripeCustomerService(); + var customer = await customerService.CreateAsync(new StripeCustomerCreateOptions + { + Description = user.Name, + Email = user.Email, + SourceToken = paymentToken + }); + + var subCreateOptions = new StripeSubscriptionCreateOptions + { + Items = new List(), + Metadata = new Dictionary + { + ["userId"] = user.Id.ToString() + } + }; + + subCreateOptions.Items.Add(new StripeSubscriptionItemOption + { + PlanId = PremiumPlanId, + Quantity = 1 + }); + + if(additionalStorageGb > 0) + { + subCreateOptions.Items.Add(new StripeSubscriptionItemOption + { + PlanId = StoragePlanId, + Quantity = additionalStorageGb + }); + } + + StripeSubscription subscription = null; + try + { + var subscriptionService = new StripeSubscriptionService(); + subscription = await subscriptionService.CreateAsync(customer.Id, subCreateOptions); + } + catch(StripeException) + { + await customerService.DeleteAsync(customer.Id); + throw; + } + + user.StripeCustomerId = customer.Id; + user.StripeSubscriptionId = subscription.Id; + } + } +} diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 5cda8568c7..8060e86cca 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -514,55 +514,12 @@ namespace Bit.Core.Services throw new BadRequestException("Already a premium user."); } - var customerService = new StripeCustomerService(); - var customer = await customerService.CreateAsync(new StripeCustomerCreateOptions - { - Description = user.Name, - Email = user.Email, - SourceToken = paymentToken - }); - - var subCreateOptions = new StripeSubscriptionCreateOptions - { - Items = new List(), - Metadata = new Dictionary - { - ["userId"] = user.Id.ToString() - } - }; - - subCreateOptions.Items.Add(new StripeSubscriptionItemOption - { - PlanId = PremiumPlanId, - Quantity = 1 - }); - - if(additionalStorageGb > 0) - { - subCreateOptions.Items.Add(new StripeSubscriptionItemOption - { - PlanId = StoragePlanId, - Quantity = additionalStorageGb - }); - } - - StripeSubscription subscription = null; - try - { - var subscriptionService = new StripeSubscriptionService(); - subscription = await subscriptionService.CreateAsync(customer.Id, subCreateOptions); - } - catch(StripeException) - { - await customerService.DeleteAsync(customer.Id); - throw; - } + IPaymentService paymentService = new StripePaymentService(_globalSettings); + await paymentService.PurchasePremiumAsync(user, paymentToken, additionalStorageGb); user.Premium = true; user.MaxStorageGb = (short)(1 + additionalStorageGb); user.RevisionDate = DateTime.UtcNow; - user.StripeCustomerId = customer.Id; - user.StripeSubscriptionId = subscription.Id; try { @@ -570,7 +527,7 @@ namespace Bit.Core.Services } catch { - await BillingHelpers.CancelAndRecoverChargesAsync(subscription.Id, customer.Id); + await BillingHelpers.CancelAndRecoverChargesAsync(user.StripeSubscriptionId, user.StripeCustomerId); throw; } } diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 4572866ba2..6f99a8fc44 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Data; +using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -16,6 +17,7 @@ namespace Bit.Core.Utilities { private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks; private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private static readonly Random _random = new Random(); /// /// Generate sequential Guid for Sql Server. @@ -128,34 +130,21 @@ namespace Bit.Core.Utilities return globalSettings.U2f.AppId; } + public static string RandomString(int length, bool alpha = true, bool upper = true, bool lower = true, + bool numeric = true, bool special = false) + { + return RandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special)); + } + + public static string RandomString(int length, string characters) + { + return new string(Enumerable.Repeat(characters, length).Select(s => s[_random.Next(s.Length)]).ToArray()); + } + public static string SecureRandomString(int length, bool alpha = true, bool upper = true, bool lower = true, bool numeric = true, bool special = false) { - var characters = string.Empty; - if(alpha) - { - if(upper) - { - characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - } - - if(lower) - { - characters += "abcdefghijklmnopqrstuvwxyz"; - } - } - - if(numeric) - { - characters += "0123456789"; - } - - if(special) - { - characters += "!@#$%^*&"; - } - - return SecureRandomString(length, characters); + return SecureRandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special)); } // ref https://stackoverflow.com/a/8996788/1090359 with modifications @@ -205,6 +194,35 @@ namespace Bit.Core.Utilities } } + private static string RandomStringCharacters(bool alpha, bool upper, bool lower, bool numeric, bool special) + { + var characters = string.Empty; + if(alpha) + { + if(upper) + { + characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + } + + if(lower) + { + characters += "abcdefghijklmnopqrstuvwxyz"; + } + } + + if(numeric) + { + characters += "0123456789"; + } + + if(special) + { + characters += "!@#$%^*&"; + } + + return characters; + } + // ref: https://stackoverflow.com/a/11124118/1090359 // Returns the human-readable file size for an arbitrary 64-bit file size . // The format is "0.## XB", ex: "4.2 KB" or "1.43 GB" diff --git a/src/Identity/settings.Production.json b/src/Identity/settings.Production.json index 08a068c6bd..57d8453827 100644 --- a/src/Identity/settings.Production.json +++ b/src/Identity/settings.Production.json @@ -3,6 +3,9 @@ "baseVaultUri": "https://vault.bitwarden.com/#", "u2f": { "appId": "https://vault.bitwarden.com/app-id.json" + }, + "braintree": { + "production": true } } } diff --git a/src/Identity/settings.json b/src/Identity/settings.json index d018cb2a32..076716345a 100644 --- a/src/Identity/settings.json +++ b/src/Identity/settings.json @@ -36,6 +36,12 @@ }, "u2f": { "appId": "https://localhost:4001/app-id.json" + }, + "braintree": { + "production": false, + "merchantId": "SECRET", + "publicKey": "SECRET", + "privateKey": "SECRET" } } }