1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-22 20:11:04 -05:00

change payment methods between stripe and paypal

This commit is contained in:
Kyle Spearrin 2019-01-31 12:11:30 -05:00
parent fca1ee4253
commit 952d624d72
8 changed files with 172 additions and 52 deletions

View File

@ -1,15 +1,18 @@
using Bit.Core.Enums; using System;
using Bit.Core.Enums;
using Bit.Core.Services; using Bit.Core.Services;
namespace Bit.Core.Models.Table namespace Bit.Core.Models.Table
{ {
public interface ISubscriber public interface ISubscriber
{ {
Guid Id { get; }
GatewayType? Gateway { get; set; } GatewayType? Gateway { get; set; }
string GatewayCustomerId { get; set; } string GatewayCustomerId { get; set; }
string GatewaySubscriptionId { get; set; } string GatewaySubscriptionId { get; set; }
string BillingEmailAddress(); string BillingEmailAddress();
string BillingName(); string BillingName();
string BraintreeCustomerIdPrefix();
IPaymentService GetPaymentService(GlobalSettings globalSettings); IPaymentService GetPaymentService(GlobalSettings globalSettings);
} }
} }

View File

@ -63,6 +63,11 @@ namespace Bit.Core.Models.Table
return BusinessName; return BusinessName;
} }
public string BraintreeCustomerIdPrefix()
{
return "o";
}
public long StorageBytesRemaining() public long StorageBytesRemaining()
{ {
if(!MaxStorageGb.HasValue) if(!MaxStorageGb.HasValue)

View File

@ -58,6 +58,11 @@ namespace Bit.Core.Models.Table
return Name; return Name;
} }
public string BraintreeCustomerIdPrefix()
{
return "u";
}
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders() public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
{ {
if(string.IsNullOrWhiteSpace(TwoFactorProviders)) if(string.IsNullOrWhiteSpace(TwoFactorProviders))

View File

@ -8,12 +8,13 @@ namespace Bit.Core.Services
public interface IPaymentService public interface IPaymentService
{ {
Task CancelAndRecoverChargesAsync(ISubscriber subscriber); Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
short additionalStorageGb); short additionalStorageGb);
Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId); Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId);
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false); Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);
Task ReinstateSubscriptionAsync(ISubscriber subscriber); Task ReinstateSubscriptionAsync(ISubscriber subscriber);
Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken); Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType,
string paymentToken);
Task<BillingInfo.BillingInvoice> GetUpcomingInvoiceAsync(ISubscriber subscriber); Task<BillingInfo.BillingInvoice> GetUpcomingInvoiceAsync(ISubscriber subscriber);
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber); Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
} }

View File

@ -215,7 +215,7 @@ namespace Bit.Core.Services
return billingInfo; return billingInfo;
} }
public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
short additionalStorageGb) short additionalStorageGb)
{ {
var customerResult = await _gateway.Customer.CreateAsync(new CustomerRequest var customerResult = await _gateway.Customer.CreateAsync(new CustomerRequest
@ -303,14 +303,20 @@ namespace Bit.Core.Services
} }
} }
public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken) public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType,
string paymentToken)
{ {
if(subscriber == null) if(subscriber == null)
{ {
throw new ArgumentNullException(nameof(subscriber)); 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. " + throw new GatewayException("Switching from one payment type to another is not supported. " +
"Contact us for assistance."); "Contact us for assistance.");

View File

@ -78,7 +78,22 @@ namespace Bit.Core.Services
throw new NotFoundException(); 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) if(updated)
{ {
await ReplaceAndUpdateCache(organization); await ReplaceAndUpdateCache(organization);
@ -340,7 +355,7 @@ namespace Bit.Core.Services
{ {
throw new BadRequestException("Subscription not found."); throw new BadRequestException("Subscription not found.");
} }
Func<bool, Task<SubscriptionItem>> subUpdateAction = null; Func<bool, Task<SubscriptionItem>> subUpdateAction = null;
var seatItem = sub.Items?.Data?.FirstOrDefault(i => i.Plan.Id == plan.StripeSeatPlanId); var seatItem = sub.Items?.Data?.FirstOrDefault(i => i.Plan.Id == plan.StripeSeatPlanId);
var subItemOptions = sub.Items.Where(i => i.Plan.Id != plan.StripeSeatPlanId) var subItemOptions = sub.Items.Where(i => i.Plan.Id != plan.StripeSeatPlanId)

View File

@ -65,6 +65,10 @@ namespace Bit.Core.Services
braintreeCustomer = customerResult.Target; braintreeCustomer = customerResult.Target;
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id); stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
} }
else
{
throw new GatewayException("Payment method is not supported at this time.");
}
var customer = await customerService.CreateAsync(new CustomerCreateOptions var customer = await customerService.CreateAsync(new CustomerCreateOptions
{ {
@ -542,20 +546,26 @@ namespace Bit.Core.Services
} }
} }
public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken) public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType,
string paymentToken)
{ {
if(subscriber == null) if(subscriber == null)
{ {
throw new ArgumentNullException(nameof(subscriber)); 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. " + throw new GatewayException("Switching from one payment type to another is not supported. " +
"Contact us for assistance."); "Contact us for assistance.");
} }
var updatedSubscriber = false; var createdCustomer = false;
Braintree.Customer braintreeCustomer = null;
string stipeCustomerSourceToken = null;
var stripeCustomerMetadata = new Dictionary<string, string>();
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
paymentMethodType == PaymentMethodType.BankAccount;
var cardService = new CardService(); var cardService = new CardService();
var bankSerice = new BankAccountService(); var bankSerice = new BankAccountService();
@ -565,53 +575,122 @@ namespace Bit.Core.Services
if(!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) if(!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
customer = await customerService.GetAsync(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(), Email = subscriber.BillingEmailAddress(),
SourceToken = paymentToken Id = subscriber.BraintreeCustomerIdPrefix() + subscriber.Id.ToString("N").ToLower() + randomSuffix
}); });
subscriber.Gateway = Enums.GatewayType.Stripe; if(!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
subscriber.GatewayCustomerId = customer.Id;
updatedSubscriber = true;
}
else
{
if(paymentToken.StartsWith("btok_"))
{ {
await bankSerice.CreateAsync(customer.Id, new BankAccountCreateOptions throw new GatewayException("Failed to create PayPal customer record.");
{ }
SourceToken = paymentToken
}); braintreeCustomer = customerResult.Target;
if(stripeCustomerMetadata.ContainsKey("btCustomerId"))
{
stripeCustomerMetadata["btCustomerId"] = braintreeCustomer.Id;
} }
else else
{ {
await cardService.CreateAsync(customer.Id, new CardCreateOptions stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
{
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);
}
} }
} }
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<BillingInfo.BillingInvoice> GetUpcomingInvoiceAsync(ISubscriber subscriber) public async Task<BillingInfo.BillingInvoice> GetUpcomingInvoiceAsync(ISubscriber subscriber)
@ -660,12 +739,17 @@ namespace Bit.Core.Services
if(customer.Metadata?.ContainsKey("btCustomerId") ?? false) if(customer.Metadata?.ContainsKey("btCustomerId") ?? false)
{ {
var braintreeCustomer = await _btGateway.Customer.FindAsync(customer.Metadata["btCustomerId"]); try
if(braintreeCustomer?.DefaultPaymentMethod != null)
{ {
billingInfo.PaymentSource = new BillingInfo.BillingSource( var braintreeCustomer = await _btGateway.Customer.FindAsync(
braintreeCustomer.DefaultPaymentMethod); 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) else if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId) && customer.Sources?.Data != null)
{ {

View File

@ -800,17 +800,18 @@ namespace Bit.Core.Services
throw new BadRequestException("Invalid token."); throw new BadRequestException("Invalid token.");
} }
IPaymentService paymentService = null; PaymentMethodType paymentMethodType;
var paymentService = new StripePaymentService(_globalSettings);
if(paymentToken.StartsWith("tok_")) if(paymentToken.StartsWith("tok_"))
{ {
paymentService = new StripePaymentService(_globalSettings); paymentMethodType = PaymentMethodType.Card;
} }
else 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) if(updated)
{ {
await SaveUserAsync(user); await SaveUserAsync(user);