1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-09 07:08:15 -05:00

support credit purchases and prorated upgrades

This commit is contained in:
Kyle Spearrin 2019-02-20 23:54:27 -05:00
parent e10c99ec96
commit 01d324a8b4
4 changed files with 76 additions and 29 deletions

View File

@ -22,7 +22,8 @@ namespace Bit.Core.Models.Api
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{ {
if(string.IsNullOrWhiteSpace(PaymentToken) && License == null) var creditType = PaymentMethodType.HasValue && PaymentMethodType.Value == Enums.PaymentMethodType.Credit;
if(string.IsNullOrWhiteSpace(PaymentToken) && !creditType && License == null)
{ {
yield return new ValidationResult("Payment token or license is required."); yield return new ValidationResult("Payment token or license is required.");
} }

View File

@ -550,7 +550,7 @@ namespace Bit.Core.Services
} }
else else
{ {
if(!signup.PaymentMethodType.HasValue) if(!signup.PaymentMethodType.HasValue && !string.IsNullOrWhiteSpace(signup.PaymentToken))
{ {
if(signup.PaymentToken.StartsWith("btok_")) if(signup.PaymentToken.StartsWith("btok_"))
{ {

View File

@ -161,6 +161,20 @@ namespace Bit.Core.Services
public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
short additionalStorageGb) short additionalStorageGb)
{ {
if(paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken))
{
throw new BadRequestException("Payment token is required.");
}
if(paymentMethodType == PaymentMethodType.Credit &&
(user.Gateway != GatewayType.Stripe || string.IsNullOrWhiteSpace(user.GatewayCustomerId)))
{
throw new BadRequestException("Your account does not have any credit available.");
}
if(paymentMethodType == PaymentMethodType.BankAccount)
{
throw new GatewayException("Bank account payment method is not supported at this time.");
}
var invoiceService = new InvoiceService(); var invoiceService = new InvoiceService();
var customerService = new CustomerService(); var customerService = new CustomerService();
@ -169,28 +183,26 @@ namespace Bit.Core.Services
Braintree.Transaction braintreeTransaction = null; Braintree.Transaction braintreeTransaction = null;
Braintree.Customer braintreeCustomer = null; Braintree.Customer braintreeCustomer = null;
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card || var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
paymentMethodType == PaymentMethodType.BankAccount; paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.Credit;
if(paymentMethodType == PaymentMethodType.BankAccount)
{
throw new GatewayException("Bank account payment method is not supported at this time.");
}
if(user.Gateway == GatewayType.Stripe && !string.IsNullOrWhiteSpace(user.GatewayCustomerId)) if(user.Gateway == GatewayType.Stripe && !string.IsNullOrWhiteSpace(user.GatewayCustomerId))
{ {
try if(!string.IsNullOrWhiteSpace(paymentToken))
{ {
await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken); try
stripeCustomerId = user.GatewayCustomerId; {
createdStripeCustomer = false; await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken);
} }
catch(Exception) catch
{ {
stripeCustomerId = null; stripeCustomerId = null;
}
} }
stripeCustomerId = user.GatewayCustomerId;
createdStripeCustomer = false;
} }
if(string.IsNullOrWhiteSpace(stripeCustomerId)) if(string.IsNullOrWhiteSpace(stripeCustomerId) && !string.IsNullOrWhiteSpace(paymentToken))
{ {
string stipeCustomerSourceToken = null; string stipeCustomerSourceToken = null;
var stripeCustomerMetadata = new Dictionary<string, string>(); var stripeCustomerMetadata = new Dictionary<string, string>();
@ -316,6 +328,18 @@ namespace Bit.Core.Services
throw new GatewayException("No payment was able to be collected."); throw new GatewayException("No payment was able to be collected.");
} }
} }
else if(paymentMethodType == PaymentMethodType.Credit)
{
var previewInvoice = await invoiceService.UpcomingAsync(new UpcomingInvoiceOptions
{
CustomerId = stripeCustomerId,
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items)
});
if(previewInvoice.AmountDue > 0)
{
throw new GatewayException("Your account does not have enough credit available.");
}
}
var subscriptionService = new SubscriptionService(); var subscriptionService = new SubscriptionService();
subscription = await subscriptionService.CreateAsync(subCreateOptions); subscription = await subscriptionService.CreateAsync(subCreateOptions);
@ -533,6 +557,20 @@ namespace Bit.Core.Services
// Owes more than prorateThreshold on next invoice. // Owes more than prorateThreshold on next invoice.
// Invoice them and pay now instead of waiting until next billing cycle. // Invoice them and pay now instead of waiting until next billing cycle.
var customerService = new CustomerService();
customerService.ExpandDefaultSource = true;
var customer = await customerService.GetAsync(subscriber.GatewayCustomerId);
var invoiceAmountDue = upcomingPreview.StartingBalance + invoiceAmount;
if(invoiceAmountDue > 0 && !customer.Metadata.ContainsKey("btCustomerId"))
{
if(customer.DefaultSource == null ||
(!(customer.DefaultSource is Card) && !(customer.DefaultSource is BankAccount)))
{
// throw new BadRequestException("No payment method is available.");
}
}
Invoice invoice = null; Invoice invoice = null;
var createdInvoiceItems = new List<InvoiceItem>(); var createdInvoiceItems = new List<InvoiceItem>();
Braintree.Transaction braintreeTransaction = null; Braintree.Transaction braintreeTransaction = null;
@ -565,14 +603,12 @@ namespace Bit.Core.Services
}); });
var invoicePayOptions = new InvoicePayOptions(); var invoicePayOptions = new InvoicePayOptions();
var customerService = new CustomerService(); if(invoice.AmountDue > 0)
var customer = await customerService.GetAsync(subscriber.GatewayCustomerId);
if(customer != null)
{ {
if(customer.Metadata.ContainsKey("btCustomerId")) if(customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
{ {
invoicePayOptions.PaidOutOfBand = true; invoicePayOptions.PaidOutOfBand = true;
var btInvoiceAmount = (invoiceAmount / 100M); var btInvoiceAmount = (invoice.AmountDue / 100M);
var transactionResult = await _btGateway.Transaction.SaleAsync( var transactionResult = await _btGateway.Transaction.SaleAsync(
new Braintree.TransactionRequest new Braintree.TransactionRequest
{ {
@ -610,7 +646,14 @@ namespace Bit.Core.Services
} }
} }
await invoiceService.PayAsync(invoice.Id, invoicePayOptions); try
{
await invoiceService.PayAsync(invoice.Id, invoicePayOptions);
}
catch(StripeException)
{
throw new GatewayException("Unable to pay invoice.");
}
} }
catch(Exception e) catch(Exception e)
{ {
@ -620,7 +663,14 @@ namespace Bit.Core.Services
} }
if(invoice != null) if(invoice != null)
{ {
await invoiceService.DeleteAsync(invoice.Id); await invoiceService.VoidInvoiceAsync(invoice.Id, new InvoiceVoidOptions());
if(invoice.StartingBalance != 0)
{
await customerService.UpdateAsync(customer.Id, new CustomerUpdateOptions
{
AccountBalance = customer.AccountBalance
});
}
// Restore invoice items that were brought in // Restore invoice items that were brought in
foreach(var item in pendingInvoiceItems) foreach(var item in pendingInvoiceItems)

View File

@ -708,7 +708,7 @@ namespace Bit.Core.Services
Directory.CreateDirectory(dir); Directory.CreateDirectory(dir);
File.WriteAllText($"{dir}/{user.Id}.json", JsonConvert.SerializeObject(license, Formatting.Indented)); File.WriteAllText($"{dir}/{user.Id}.json", JsonConvert.SerializeObject(license, Formatting.Indented));
} }
else if(!string.IsNullOrWhiteSpace(paymentToken)) else
{ {
if(!paymentMethodType.HasValue) if(!paymentMethodType.HasValue)
{ {
@ -729,10 +729,6 @@ namespace Bit.Core.Services
await _paymentService.PurchasePremiumAsync(user, paymentMethodType.Value, await _paymentService.PurchasePremiumAsync(user, paymentMethodType.Value,
paymentToken, additionalStorageGb); paymentToken, additionalStorageGb);
} }
else
{
throw new InvalidOperationException("License or payment token is required.");
}
user.Premium = true; user.Premium = true;
user.RevisionDate = DateTime.UtcNow; user.RevisionDate = DateTime.UtcNow;