1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 16:12:49 -05:00

refactor for addtnl. payment service (braintree)

This commit is contained in:
Kyle Spearrin
2017-07-28 12:09:12 -04:00
parent 2dc9c196c4
commit 082b53e133
14 changed files with 928 additions and 351 deletions

View File

@ -1,12 +1,17 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Braintree;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
namespace Bit.Core.Services
{
public class BraintreePaymentService : IPaymentService
{
private const string PremiumPlanId = "premium-annually";
private const string StoragePlanId = "storage-gb-annually";
private readonly BraintreeGateway _gateway;
public BraintreePaymentService(
@ -22,6 +27,172 @@ namespace Bit.Core.Services
};
}
public async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId)
{
var sub = await _gateway.Subscription.FindAsync(storableSubscriber.StripeSubscriptionId);
if(sub == null)
{
throw new GatewayException("Subscription was not found.");
}
var req = new SubscriptionRequest
{
AddOns = new AddOnsRequest(),
Options = new SubscriptionOptionsRequest
{
ProrateCharges = true
}
};
var storageItem = sub.AddOns?.FirstOrDefault(a => a.Id == storagePlanId);
if(additionalStorage > 0 && storageItem == null)
{
req.AddOns.Add = new AddAddOnRequest[]
{
new AddAddOnRequest
{
InheritedFromId = storagePlanId,
Quantity = additionalStorage,
NeverExpires = true
}
};
}
else if(additionalStorage > 0 && storageItem != null)
{
req.AddOns.Update = new UpdateAddOnRequest[]
{
new UpdateAddOnRequest
{
ExistingId = storageItem.Id,
Quantity = additionalStorage
}
};
}
else if(additionalStorage == 0 && storageItem != null)
{
req.AddOns.Remove = new string[] { storageItem.Id };
}
var result = await _gateway.Subscription.UpdateAsync(sub.Id, req);
if(!result.IsSuccess())
{
throw new GatewayException("Failed to adjust storage.");
}
}
public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber)
{
if(!string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
{
await _gateway.Subscription.CancelAsync(subscriber.StripeSubscriptionId);
}
if(string.IsNullOrWhiteSpace(subscriber.StripeCustomerId))
{
return;
}
var transactionRequest = new TransactionSearchRequest().CustomerId.Is(subscriber.StripeCustomerId);
var transactions = _gateway.Transaction.Search(transactionRequest);
if((transactions?.MaximumCount ?? 0) > 0)
{
foreach(var transaction in transactions.Cast<Transaction>().Where(c => c.RefundedTransactionId == null))
{
await _gateway.Transaction.RefundAsync(transaction.Id);
}
}
await _gateway.Customer.DeleteAsync(subscriber.StripeCustomerId);
}
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false)
{
if(subscriber == null)
{
throw new ArgumentNullException(nameof(subscriber));
}
if(string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
{
throw new GatewayException("No subscription.");
}
var sub = await _gateway.Subscription.FindAsync(subscriber.StripeSubscriptionId);
if(sub == null)
{
throw new GatewayException("Subscription was not found.");
}
if(sub.Status == SubscriptionStatus.CANCELED || sub.Status == SubscriptionStatus.EXPIRED ||
!sub.NeverExpires.GetValueOrDefault())
{
throw new GatewayException("Subscription is already canceled.");
}
if(endOfPeriod)
{
var req = new SubscriptionRequest
{
NeverExpires = false,
NumberOfBillingCycles = sub.CurrentBillingCycle
};
var result = await _gateway.Subscription.UpdateAsync(subscriber.StripeSubscriptionId, req);
if(!result.IsSuccess())
{
throw new GatewayException("Unable to cancel subscription.");
}
}
else
{
var result = await _gateway.Subscription.CancelAsync(subscriber.StripeSubscriptionId);
if(!result.IsSuccess())
{
throw new GatewayException("Unable to cancel subscription.");
}
}
}
public async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber)
{
var billingInfo = new BillingInfo();
if(!string.IsNullOrWhiteSpace(subscriber.StripeCustomerId))
{
var customer = await _gateway.Customer.FindAsync(subscriber.StripeCustomerId);
if(customer != null)
{
if(customer.DefaultPaymentMethod != null)
{
billingInfo.PaymentSource = new BillingInfo.BillingSource(customer.DefaultPaymentMethod);
}
var transactionRequest = new TransactionSearchRequest().CustomerId.Is(customer.Id);
var transactions = _gateway.Transaction.Search(transactionRequest);
billingInfo.Charges = transactions?.Cast<Transaction>().OrderByDescending(t => t.CreatedAt)
.Select(t => new BillingInfo.BillingCharge(t));
}
}
if(!string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
{
var sub = await _gateway.Subscription.FindAsync(subscriber.StripeSubscriptionId);
if(sub != null)
{
var plans = await _gateway.Plan.AllAsync();
var plan = plans?.FirstOrDefault(p => p.Id == sub.PlanId);
billingInfo.Subscription = new BillingInfo.BillingSubscription(sub, plan);
}
if(sub.NextBillingDate.HasValue)
{
billingInfo.UpcomingInvoice = new BillingInfo.BillingInvoice(sub);
}
}
return billingInfo;
}
public async Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb)
{
var customerResult = await _gateway.Customer.CreateAsync(new CustomerRequest
@ -30,9 +201,9 @@ namespace Bit.Core.Services
Email = user.Email
});
if(!customerResult.IsSuccess())
if(!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
{
// error, throw something
throw new GatewayException("Failed to create customer.");
}
var subId = "u" + user.Id.ToString("N").ToLower() +
@ -41,17 +212,18 @@ namespace Bit.Core.Services
var subRequest = new SubscriptionRequest
{
Id = subId,
PaymentMethodToken = paymentToken,
PlanId = "premium-annually"
PaymentMethodToken = customerResult.Target.PaymentMethods[0].Token,
PlanId = PremiumPlanId
};
if(additionalStorageGb > 0)
{
subRequest.AddOns = new AddOnsRequest();
subRequest.AddOns.Add = new AddAddOnRequest[]
{
new AddAddOnRequest
{
InheritedFromId = "storage-gb-annually",
InheritedFromId = StoragePlanId,
Quantity = additionalStorageGb
}
};
@ -62,11 +234,104 @@ namespace Bit.Core.Services
if(!subResult.IsSuccess())
{
await _gateway.Customer.DeleteAsync(customerResult.Target.Id);
// error, throw something
throw new GatewayException("Failed to create subscription.");
}
user.StripeCustomerId = customerResult.Target.Id;
user.StripeSubscriptionId = subResult.Target.Id;
}
public async Task ReinstateSubscriptionAsync(ISubscriber subscriber)
{
if(subscriber == null)
{
throw new ArgumentNullException(nameof(subscriber));
}
if(string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
{
throw new GatewayException("No subscription.");
}
var sub = await _gateway.Subscription.FindAsync(subscriber.StripeSubscriptionId);
if(sub == null)
{
throw new GatewayException("Subscription was not found.");
}
if(sub.Status != SubscriptionStatus.ACTIVE || sub.NeverExpires.GetValueOrDefault())
{
throw new GatewayException("Subscription is not marked for cancellation.");
}
var req = new SubscriptionRequest
{
NeverExpires = true,
NumberOfBillingCycles = null
};
var result = await _gateway.Subscription.UpdateAsync(subscriber.StripeSubscriptionId, req);
if(!result.IsSuccess())
{
throw new GatewayException("Unable to reinstate subscription.");
}
}
public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken)
{
if(subscriber == null)
{
throw new ArgumentNullException(nameof(subscriber));
}
var updatedSubscriber = false;
Customer customer = null;
if(!string.IsNullOrWhiteSpace(subscriber.StripeCustomerId))
{
customer = await _gateway.Customer.FindAsync(subscriber.StripeCustomerId);
}
if(customer == null)
{
var result = await _gateway.Customer.CreateAsync(new CustomerRequest
{
Email = subscriber.BillingEmailAddress(),
PaymentMethodNonce = paymentToken
});
if(!result.IsSuccess())
{
throw new GatewayException("Cannot create customer.");
}
customer = result.Target;
subscriber.StripeCustomerId = customer.Id;
updatedSubscriber = true;
}
else
{
if(customer.DefaultPaymentMethod != null)
{
var deleteResult = await _gateway.PaymentMethod.DeleteAsync(customer.DefaultPaymentMethod.Token);
if(!deleteResult.IsSuccess())
{
throw new GatewayException("Cannot delete old payment method.");
}
}
var result = await _gateway.PaymentMethod.CreateAsync(new PaymentMethodRequest
{
PaymentMethodNonce = paymentToken,
CustomerId = customer.Id
});
if(!result.IsSuccess())
{
throw new GatewayException("Cannot add new payment method.");
}
}
return updatedSubscriber;
}
}
}

View File

@ -25,6 +25,7 @@ namespace Bit.Core.Services
private readonly IMailService _mailService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IPushRegistrationService _pushRegistrationService;
private readonly StripePaymentService _stripePaymentService;
public OrganizationService(
IOrganizationRepository organizationRepository,
@ -46,6 +47,7 @@ namespace Bit.Core.Services
_mailService = mailService;
_pushNotificationService = pushNotificationService;
_pushRegistrationService = pushRegistrationService;
_stripePaymentService = new StripePaymentService();
}
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken)
@ -56,7 +58,7 @@ namespace Bit.Core.Services
throw new NotFoundException();
}
var updated = await BillingHelpers.UpdatePaymentMethodAsync(organization, paymentToken);
var updated = await _stripePaymentService.UpdatePaymentMethodAsync(organization, paymentToken);
if(updated)
{
await _organizationRepository.ReplaceAsync(organization);
@ -71,7 +73,7 @@ namespace Bit.Core.Services
throw new NotFoundException();
}
await BillingHelpers.CancelSubscriptionAsync(organization, endOfPeriod);
await _stripePaymentService.CancelSubscriptionAsync(organization, endOfPeriod);
}
public async Task ReinstateSubscriptionAsync(Guid organizationId)
@ -82,7 +84,7 @@ namespace Bit.Core.Services
throw new NotFoundException();
}
await BillingHelpers.ReinstateSubscriptionAsync(organization);
await _stripePaymentService.ReinstateSubscriptionAsync(organization);
}
public async Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats)
@ -241,7 +243,8 @@ namespace Bit.Core.Services
throw new BadRequestException("Plan does not allow additional storage.");
}
await BillingHelpers.AdjustStorageAsync(organization, storageAdjustmentGb, plan.StripStoragePlanId);
await BillingHelpers.AdjustStorageAsync(_stripePaymentService, organization, storageAdjustmentGb,
plan.StripStoragePlanId);
await _organizationRepository.ReplaceAsync(organization);
}
@ -337,7 +340,7 @@ namespace Bit.Core.Services
if(additionalSeats > 0)
{
await BillingHelpers.PreviewUpcomingInvoiceAndPayAsync(organization, plan.StripeSeatPlanId, 500);
await _stripePaymentService.PreviewUpcomingInvoiceAndPayAsync(organization, plan.StripeSeatPlanId, 500);
}
organization.Seats = (short?)newSeatTotal;
@ -500,7 +503,7 @@ namespace Bit.Core.Services
}
catch
{
await BillingHelpers.CancelAndRecoverChargesAsync(subscription?.Id, customer?.Id);
await _stripePaymentService.CancelAndRecoverChargesAsync(organization);
if(organization.Id != default(Guid))
{
await _organizationRepository.DeleteAsync(organization);

View File

@ -3,6 +3,9 @@ using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Stripe;
using System.Collections.Generic;
using Bit.Core.Exceptions;
using System.Linq;
using Bit.Core.Models.Business;
namespace Bit.Core.Services
{
@ -11,12 +14,6 @@ namespace Bit.Core.Services
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();
@ -66,5 +63,290 @@ namespace Bit.Core.Services
user.StripeCustomerId = customer.Id;
user.StripeSubscriptionId = subscription.Id;
}
public async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage,
string storagePlanId)
{
var subscriptionItemService = new StripeSubscriptionItemService();
var subscriptionService = new StripeSubscriptionService();
var sub = await subscriptionService.GetAsync(storableSubscriber.StripeSubscriptionId);
if(sub == null)
{
throw new GatewayException("Subscription not found.");
}
var storageItem = sub.Items?.Data?.FirstOrDefault(i => i.Plan.Id == storagePlanId);
if(additionalStorage > 0 && storageItem == null)
{
await subscriptionItemService.CreateAsync(new StripeSubscriptionItemCreateOptions
{
PlanId = storagePlanId,
Quantity = additionalStorage,
Prorate = true,
SubscriptionId = sub.Id
});
}
else if(additionalStorage > 0 && storageItem != null)
{
await subscriptionItemService.UpdateAsync(storageItem.Id, new StripeSubscriptionItemUpdateOptions
{
PlanId = storagePlanId,
Quantity = additionalStorage,
Prorate = true
});
}
else if(additionalStorage == 0 && storageItem != null)
{
await subscriptionItemService.DeleteAsync(storageItem.Id);
}
if(additionalStorage > 0)
{
await PreviewUpcomingInvoiceAndPayAsync(storableSubscriber, storagePlanId, 400);
}
}
public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber)
{
if(!string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
{
var subscriptionService = new StripeSubscriptionService();
await subscriptionService.CancelAsync(subscriber.StripeSubscriptionId, false);
}
if(string.IsNullOrWhiteSpace(subscriber.StripeCustomerId))
{
return;
}
var chargeService = new StripeChargeService();
var charges = await chargeService.ListAsync(new StripeChargeListOptions
{
CustomerId = subscriber.StripeCustomerId
});
if(charges?.Data != null)
{
var refundService = new StripeRefundService();
foreach(var charge in charges.Data.Where(c => !c.Refunded))
{
await refundService.CreateAsync(charge.Id);
}
}
var customerService = new StripeCustomerService();
await customerService.DeleteAsync(subscriber.StripeCustomerId);
}
public async Task PreviewUpcomingInvoiceAndPayAsync(ISubscriber subscriber, string planId,
int prorateThreshold = 500)
{
var invoiceService = new StripeInvoiceService();
var upcomingPreview = await invoiceService.UpcomingAsync(subscriber.StripeCustomerId,
new StripeUpcomingInvoiceOptions
{
SubscriptionId = subscriber.StripeSubscriptionId
});
var prorationAmount = upcomingPreview.StripeInvoiceLineItems?.Data?
.TakeWhile(i => i.Plan.Id == planId && i.Proration).Sum(i => i.Amount);
if(prorationAmount.GetValueOrDefault() >= prorateThreshold)
{
try
{
// Owes more than prorateThreshold on next invoice.
// Invoice them and pay now instead of waiting until next month.
var invoice = await invoiceService.CreateAsync(subscriber.StripeCustomerId,
new StripeInvoiceCreateOptions
{
SubscriptionId = subscriber.StripeSubscriptionId
});
if(invoice.AmountDue > 0)
{
await invoiceService.PayAsync(invoice.Id);
}
}
catch(StripeException) { }
}
}
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false)
{
if(subscriber == null)
{
throw new ArgumentNullException(nameof(subscriber));
}
if(string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
{
throw new GatewayException("No subscription.");
}
var subscriptionService = new StripeSubscriptionService();
var sub = await subscriptionService.GetAsync(subscriber.StripeSubscriptionId);
if(sub == null)
{
throw new GatewayException("Subscription was not found.");
}
if(sub.CanceledAt.HasValue)
{
throw new GatewayException("Subscription is already canceled.");
}
var canceledSub = await subscriptionService.CancelAsync(sub.Id, endOfPeriod);
if(!canceledSub.CanceledAt.HasValue)
{
throw new GatewayException("Unable to cancel subscription.");
}
}
public async Task ReinstateSubscriptionAsync(ISubscriber subscriber)
{
if(subscriber == null)
{
throw new ArgumentNullException(nameof(subscriber));
}
if(string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
{
throw new GatewayException("No subscription.");
}
var subscriptionService = new StripeSubscriptionService();
var sub = await subscriptionService.GetAsync(subscriber.StripeSubscriptionId);
if(sub == null)
{
throw new GatewayException("Subscription was not found.");
}
if((sub.Status != "active" && sub.Status != "trialing") || !sub.CanceledAt.HasValue)
{
throw new GatewayException("Subscription is not marked for cancellation.");
}
// Just touch the subscription.
var updatedSub = await subscriptionService.UpdateAsync(sub.Id, new StripeSubscriptionUpdateOptions { });
if(updatedSub.CanceledAt.HasValue)
{
throw new GatewayException("Unable to reinstate subscription.");
}
}
public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken)
{
if(subscriber == null)
{
throw new ArgumentNullException(nameof(subscriber));
}
var updatedSubscriber = false;
var cardService = new StripeCardService();
var customerService = new StripeCustomerService();
StripeCustomer customer = null;
if(!string.IsNullOrWhiteSpace(subscriber.StripeCustomerId))
{
customer = await customerService.GetAsync(subscriber.StripeCustomerId);
}
if(customer == null)
{
customer = await customerService.CreateAsync(new StripeCustomerCreateOptions
{
Description = subscriber.BillingName(),
Email = subscriber.BillingEmailAddress(),
SourceToken = paymentToken
});
subscriber.StripeCustomerId = customer.Id;
updatedSubscriber = true;
}
else
{
await cardService.CreateAsync(customer.Id, new StripeCardCreateOptions
{
SourceToken = paymentToken
});
if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId))
{
await cardService.DeleteAsync(customer.Id, customer.DefaultSourceId);
}
}
return updatedSubscriber;
}
public async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber)
{
var billingInfo = new BillingInfo();
var customerService = new StripeCustomerService();
var subscriptionService = new StripeSubscriptionService();
var chargeService = new StripeChargeService();
var invoiceService = new StripeInvoiceService();
if(!string.IsNullOrWhiteSpace(subscriber.StripeCustomerId))
{
var customer = await customerService.GetAsync(subscriber.StripeCustomerId);
if(customer != null)
{
if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId) && customer.Sources?.Data != null)
{
if(customer.DefaultSourceId.StartsWith("card_"))
{
var source = customer.Sources.Data.FirstOrDefault(s => s.Card?.Id == customer.DefaultSourceId);
if(source != null)
{
billingInfo.PaymentSource = new BillingInfo.BillingSource(source);
}
}
else if(customer.DefaultSourceId.StartsWith("ba_"))
{
var source = customer.Sources.Data
.FirstOrDefault(s => s.BankAccount?.Id == customer.DefaultSourceId);
if(source != null)
{
billingInfo.PaymentSource = new BillingInfo.BillingSource(source);
}
}
}
var charges = await chargeService.ListAsync(new StripeChargeListOptions
{
CustomerId = customer.Id,
Limit = 20
});
billingInfo.Charges = charges?.Data?.OrderByDescending(c => c.Created)
.Select(c => new BillingInfo.BillingCharge(c));
}
}
if(!string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
{
var sub = await subscriptionService.GetAsync(subscriber.StripeSubscriptionId);
if(sub != null)
{
billingInfo.Subscription = new BillingInfo.BillingSubscription(sub);
}
if(!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.StripeCustomerId))
{
try
{
var upcomingInvoice = await invoiceService.UpcomingAsync(subscriber.StripeCustomerId);
if(upcomingInvoice != null)
{
billingInfo.UpcomingInvoice = new BillingInfo.BillingInvoice(upcomingInvoice);
}
}
catch(StripeException) { }
}
}
return billingInfo;
}
}
}

View File

@ -514,7 +514,16 @@ namespace Bit.Core.Services
throw new BadRequestException("Already a premium user.");
}
IPaymentService paymentService = new StripePaymentService(_globalSettings);
IPaymentService paymentService = null;
if(paymentToken.StartsWith("tok_"))
{
paymentService = new StripePaymentService();
}
else
{
paymentService = new BraintreePaymentService(_globalSettings);
}
await paymentService.PurchasePremiumAsync(user, paymentToken, additionalStorageGb);
user.Premium = true;
@ -527,7 +536,7 @@ namespace Bit.Core.Services
}
catch
{
await BillingHelpers.CancelAndRecoverChargesAsync(user.StripeSubscriptionId, user.StripeCustomerId);
await paymentService.CancelAndRecoverChargesAsync(user);
throw;
}
}
@ -544,13 +553,15 @@ namespace Bit.Core.Services
throw new BadRequestException("Not a premium user.");
}
await BillingHelpers.AdjustStorageAsync(user, storageAdjustmentGb, StoragePlanId);
var paymentService = user.GetPaymentService(_globalSettings);
await BillingHelpers.AdjustStorageAsync(paymentService, user, storageAdjustmentGb, StoragePlanId);
await SaveUserAsync(user);
}
public async Task ReplacePaymentMethodAsync(User user, string paymentToken)
{
var updated = await BillingHelpers.UpdatePaymentMethodAsync(user, paymentToken);
var paymentService = user.GetPaymentService(_globalSettings);
var updated = await paymentService.UpdatePaymentMethodAsync(user, paymentToken);
if(updated)
{
await SaveUserAsync(user);
@ -559,12 +570,14 @@ namespace Bit.Core.Services
public async Task CancelPremiumAsync(User user, bool endOfPeriod = false)
{
await BillingHelpers.CancelSubscriptionAsync(user, endOfPeriod);
var paymentService = user.GetPaymentService(_globalSettings);
await paymentService.CancelSubscriptionAsync(user, endOfPeriod);
}
public async Task ReinstatePremiumAsync(User user)
{
await BillingHelpers.ReinstateSubscriptionAsync(user);
var paymentService = user.GetPaymentService(_globalSettings);
await paymentService.ReinstateSubscriptionAsync(user);
}
public async Task DisablePremiumAsync(Guid userId)