1
0
mirror of https://github.com/bitwarden/server.git synced 2025-01-22 21:51:22 +01:00

billing fixes and added gateway to subscriber

This commit is contained in:
Kyle Spearrin 2017-07-28 14:24:07 -04:00
parent 082b53e133
commit cfc80f8d1e
19 changed files with 269 additions and 123 deletions

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum GatewayType : byte
{
Stripe = 0,
Braintree = 1
}
}

View File

@ -2,9 +2,9 @@
{ {
public enum PaymentMethodType : byte public enum PaymentMethodType : byte
{ {
Card, Card = 0,
BankAccount, BankAccount = 1,
PayPal, PayPal = 2,
Bitcoin Bitcoin = 3
} }
} }

View File

@ -55,7 +55,7 @@ namespace Bit.Core.Models.Api
EndDate = sub.EndDate; EndDate = sub.EndDate;
CancelledDate = sub.CancelledDate; CancelledDate = sub.CancelledDate;
CancelAtEndDate = sub.CancelAtEndDate; CancelAtEndDate = sub.CancelAtEndDate;
Cancelled = Cancelled; Cancelled = sub.Cancelled;
if(sub.Items != null) if(sub.Items != null)
{ {
Items = sub.Items.Select(i => new BillingSubscriptionItem(i)); Items = sub.Items.Select(i => new BillingSubscriptionItem(i));

View File

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

View File

@ -1,6 +1,8 @@
using System; using System;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Exceptions;
namespace Bit.Core.Models.Table namespace Bit.Core.Models.Table
{ {
@ -19,8 +21,9 @@ namespace Bit.Core.Models.Table
public bool UseTotp { get; set; } public bool UseTotp { get; set; }
public long? Storage { get; set; } public long? Storage { get; set; }
public short? MaxStorageGb { get; set; } public short? MaxStorageGb { get; set; }
public string StripeCustomerId { get; set; } public GatewayType? Gateway { get; set; }
public string StripeSubscriptionId { get; set; } public string GatewayCustomerId { get; set; }
public string GatewaySubscriptionId { get; set; }
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
@ -63,5 +66,28 @@ namespace Bit.Core.Models.Table
return maxStorageBytes - Storage.Value; return maxStorageBytes - Storage.Value;
} }
public IPaymentService GetPaymentService(GlobalSettings globalSettings)
{
if(Gateway == null)
{
throw new BadRequestException("No gateway.");
}
IPaymentService paymentService = null;
switch(Gateway)
{
case GatewayType.Stripe:
paymentService = new StripePaymentService();
break;
case GatewayType.Braintree:
paymentService = new BraintreePaymentService(globalSettings);
break;
default:
throw new NotSupportedException("Unsupported gateway.");
}
return paymentService;
}
} }
} }

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Linq; using System.Linq;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Exceptions;
namespace Bit.Core.Models.Table namespace Bit.Core.Models.Table
{ {
@ -31,8 +32,9 @@ namespace Bit.Core.Models.Table
public bool Premium { get; set; } public bool Premium { get; set; }
public long? Storage { get; set; } public long? Storage { get; set; }
public short? MaxStorageGb { get; set; } public short? MaxStorageGb { get; set; }
public string StripeCustomerId { get; set; } public GatewayType? Gateway { get; set; }
public string StripeSubscriptionId { get; set; } public string GatewayCustomerId { get; set; }
public string GatewaySubscriptionId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
@ -139,14 +141,22 @@ namespace Bit.Core.Models.Table
public IPaymentService GetPaymentService(GlobalSettings globalSettings) public IPaymentService GetPaymentService(GlobalSettings globalSettings)
{ {
IPaymentService paymentService = null; if(Gateway == null)
if(StripeSubscriptionId.StartsWith("sub_"))
{ {
paymentService = new StripePaymentService(); throw new BadRequestException("No gateway.");
} }
else
IPaymentService paymentService = null;
switch(Gateway)
{ {
paymentService = new BraintreePaymentService(globalSettings); case GatewayType.Stripe:
paymentService = new StripePaymentService();
break;
case GatewayType.Braintree:
paymentService = new BraintreePaymentService(globalSettings);
break;
default:
throw new NotSupportedException("Unsupported gateway.");
} }
return paymentService; return paymentService;

View File

@ -29,7 +29,7 @@ namespace Bit.Core.Services
public async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId) public async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId)
{ {
var sub = await _gateway.Subscription.FindAsync(storableSubscriber.StripeSubscriptionId); var sub = await _gateway.Subscription.FindAsync(storableSubscriber.GatewaySubscriptionId);
if(sub == null) if(sub == null)
{ {
throw new GatewayException("Subscription was not found."); throw new GatewayException("Subscription was not found.");
@ -82,17 +82,17 @@ namespace Bit.Core.Services
public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber) public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber)
{ {
if(!string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId)) if(!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
{ {
await _gateway.Subscription.CancelAsync(subscriber.StripeSubscriptionId); await _gateway.Subscription.CancelAsync(subscriber.GatewaySubscriptionId);
} }
if(string.IsNullOrWhiteSpace(subscriber.StripeCustomerId)) if(string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
return; return;
} }
var transactionRequest = new TransactionSearchRequest().CustomerId.Is(subscriber.StripeCustomerId); var transactionRequest = new TransactionSearchRequest().CustomerId.Is(subscriber.GatewayCustomerId);
var transactions = _gateway.Transaction.Search(transactionRequest); var transactions = _gateway.Transaction.Search(transactionRequest);
if((transactions?.MaximumCount ?? 0) > 0) if((transactions?.MaximumCount ?? 0) > 0)
@ -103,7 +103,7 @@ namespace Bit.Core.Services
} }
} }
await _gateway.Customer.DeleteAsync(subscriber.StripeCustomerId); await _gateway.Customer.DeleteAsync(subscriber.GatewayCustomerId);
} }
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false) public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false)
@ -113,12 +113,12 @@ namespace Bit.Core.Services
throw new ArgumentNullException(nameof(subscriber)); throw new ArgumentNullException(nameof(subscriber));
} }
if(string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId)) if(string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
{ {
throw new GatewayException("No subscription."); throw new GatewayException("No subscription.");
} }
var sub = await _gateway.Subscription.FindAsync(subscriber.StripeSubscriptionId); var sub = await _gateway.Subscription.FindAsync(subscriber.GatewaySubscriptionId);
if(sub == null) if(sub == null)
{ {
throw new GatewayException("Subscription was not found."); throw new GatewayException("Subscription was not found.");
@ -138,7 +138,7 @@ namespace Bit.Core.Services
NumberOfBillingCycles = sub.CurrentBillingCycle NumberOfBillingCycles = sub.CurrentBillingCycle
}; };
var result = await _gateway.Subscription.UpdateAsync(subscriber.StripeSubscriptionId, req); var result = await _gateway.Subscription.UpdateAsync(subscriber.GatewaySubscriptionId, req);
if(!result.IsSuccess()) if(!result.IsSuccess())
{ {
throw new GatewayException("Unable to cancel subscription."); throw new GatewayException("Unable to cancel subscription.");
@ -146,7 +146,7 @@ namespace Bit.Core.Services
} }
else else
{ {
var result = await _gateway.Subscription.CancelAsync(subscriber.StripeSubscriptionId); var result = await _gateway.Subscription.CancelAsync(subscriber.GatewaySubscriptionId);
if(!result.IsSuccess()) if(!result.IsSuccess())
{ {
throw new GatewayException("Unable to cancel subscription."); throw new GatewayException("Unable to cancel subscription.");
@ -157,9 +157,9 @@ namespace Bit.Core.Services
public async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber) public async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber)
{ {
var billingInfo = new BillingInfo(); var billingInfo = new BillingInfo();
if(!string.IsNullOrWhiteSpace(subscriber.StripeCustomerId)) if(!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
var customer = await _gateway.Customer.FindAsync(subscriber.StripeCustomerId); var customer = await _gateway.Customer.FindAsync(subscriber.GatewayCustomerId);
if(customer != null) if(customer != null)
{ {
if(customer.DefaultPaymentMethod != null) if(customer.DefaultPaymentMethod != null)
@ -174,9 +174,9 @@ namespace Bit.Core.Services
} }
} }
if(!string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId)) if(!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
{ {
var sub = await _gateway.Subscription.FindAsync(subscriber.StripeSubscriptionId); var sub = await _gateway.Subscription.FindAsync(subscriber.GatewaySubscriptionId);
if(sub != null) if(sub != null)
{ {
var plans = await _gateway.Plan.AllAsync(); var plans = await _gateway.Plan.AllAsync();
@ -184,7 +184,8 @@ namespace Bit.Core.Services
billingInfo.Subscription = new BillingInfo.BillingSubscription(sub, plan); billingInfo.Subscription = new BillingInfo.BillingSubscription(sub, plan);
} }
if(sub.NextBillingDate.HasValue) if(!billingInfo.Subscription.Cancelled && !billingInfo.Subscription.CancelAtEndDate &&
sub.NextBillingDate.HasValue)
{ {
billingInfo.UpcomingInvoice = new BillingInfo.BillingInvoice(sub); billingInfo.UpcomingInvoice = new BillingInfo.BillingInvoice(sub);
} }
@ -237,8 +238,9 @@ namespace Bit.Core.Services
throw new GatewayException("Failed to create subscription."); throw new GatewayException("Failed to create subscription.");
} }
user.StripeCustomerId = customerResult.Target.Id; user.Gateway = Enums.GatewayType.Braintree;
user.StripeSubscriptionId = subResult.Target.Id; user.GatewayCustomerId = customerResult.Target.Id;
user.GatewaySubscriptionId = subResult.Target.Id;
} }
public async Task ReinstateSubscriptionAsync(ISubscriber subscriber) public async Task ReinstateSubscriptionAsync(ISubscriber subscriber)
@ -248,12 +250,12 @@ namespace Bit.Core.Services
throw new ArgumentNullException(nameof(subscriber)); throw new ArgumentNullException(nameof(subscriber));
} }
if(string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId)) if(string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
{ {
throw new GatewayException("No subscription."); throw new GatewayException("No subscription.");
} }
var sub = await _gateway.Subscription.FindAsync(subscriber.StripeSubscriptionId); var sub = await _gateway.Subscription.FindAsync(subscriber.GatewaySubscriptionId);
if(sub == null) if(sub == null)
{ {
throw new GatewayException("Subscription was not found."); throw new GatewayException("Subscription was not found.");
@ -270,7 +272,7 @@ namespace Bit.Core.Services
NumberOfBillingCycles = null NumberOfBillingCycles = null
}; };
var result = await _gateway.Subscription.UpdateAsync(subscriber.StripeSubscriptionId, req); var result = await _gateway.Subscription.UpdateAsync(subscriber.GatewaySubscriptionId, req);
if(!result.IsSuccess()) if(!result.IsSuccess())
{ {
throw new GatewayException("Unable to reinstate subscription."); throw new GatewayException("Unable to reinstate subscription.");
@ -284,12 +286,18 @@ namespace Bit.Core.Services
throw new ArgumentNullException(nameof(subscriber)); throw new ArgumentNullException(nameof(subscriber));
} }
if(subscriber.Gateway.HasValue && subscriber.Gateway.Value != Enums.GatewayType.Braintree)
{
throw new GatewayException("Switching from one payment type to another is not supported. " +
"Contact us for assistance.");
}
var updatedSubscriber = false; var updatedSubscriber = false;
Customer customer = null; Customer customer = null;
if(!string.IsNullOrWhiteSpace(subscriber.StripeCustomerId)) if(!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
customer = await _gateway.Customer.FindAsync(subscriber.StripeCustomerId); customer = await _gateway.Customer.FindAsync(subscriber.GatewayCustomerId);
} }
if(customer == null) if(customer == null)
@ -306,7 +314,8 @@ namespace Bit.Core.Services
} }
customer = result.Target; customer = result.Target;
subscriber.StripeCustomerId = customer.Id; subscriber.Gateway = Enums.GatewayType.Braintree;
subscriber.GatewayCustomerId = customer.Id;
updatedSubscriber = true; updatedSubscriber = true;
} }
else else

View File

@ -95,7 +95,7 @@ namespace Bit.Core.Services
throw new NotFoundException(); throw new NotFoundException();
} }
if(string.IsNullOrWhiteSpace(organization.StripeCustomerId)) if(string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{ {
throw new BadRequestException("No payment method found."); throw new BadRequestException("No payment method found.");
} }
@ -160,7 +160,7 @@ namespace Bit.Core.Services
// TODO: Groups? // TODO: Groups?
var subscriptionService = new StripeSubscriptionService(); var subscriptionService = new StripeSubscriptionService();
if(string.IsNullOrWhiteSpace(organization.StripeSubscriptionId)) if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{ {
// They must have been on a free plan. Create new sub. // They must have been on a free plan. Create new sub.
var subCreateOptions = new StripeSubscriptionCreateOptions var subCreateOptions = new StripeSubscriptionCreateOptions
@ -190,7 +190,7 @@ namespace Bit.Core.Services
}); });
} }
await subscriptionService.CreateAsync(organization.StripeCustomerId, subCreateOptions); await subscriptionService.CreateAsync(organization.GatewayCustomerId, subCreateOptions);
} }
else else
{ {
@ -218,7 +218,7 @@ namespace Bit.Core.Services
}); });
} }
await subscriptionService.UpdateAsync(organization.StripeSubscriptionId, subUpdateOptions); await subscriptionService.UpdateAsync(organization.GatewaySubscriptionId, subUpdateOptions);
} }
// TODO: Update organization // TODO: Update organization
@ -256,12 +256,12 @@ namespace Bit.Core.Services
throw new NotFoundException(); throw new NotFoundException();
} }
if(string.IsNullOrWhiteSpace(organization.StripeCustomerId)) if(string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{ {
throw new BadRequestException("No payment method found."); throw new BadRequestException("No payment method found.");
} }
if(string.IsNullOrWhiteSpace(organization.StripeSubscriptionId)) if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{ {
throw new BadRequestException("No subscription found."); throw new BadRequestException("No subscription found.");
} }
@ -307,7 +307,7 @@ namespace Bit.Core.Services
var subscriptionItemService = new StripeSubscriptionItemService(); var subscriptionItemService = new StripeSubscriptionItemService();
var subscriptionService = new StripeSubscriptionService(); var subscriptionService = new StripeSubscriptionService();
var sub = await subscriptionService.GetAsync(organization.StripeSubscriptionId); var sub = await subscriptionService.GetAsync(organization.GatewaySubscriptionId);
if(sub == null) if(sub == null)
{ {
throw new BadRequestException("Subscription not found."); throw new BadRequestException("Subscription not found.");
@ -469,8 +469,9 @@ namespace Bit.Core.Services
UseDirectory = plan.UseDirectory, UseDirectory = plan.UseDirectory,
UseTotp = plan.UseTotp, UseTotp = plan.UseTotp,
Plan = plan.Name, Plan = plan.Name,
StripeCustomerId = customer?.Id, Gateway = GatewayType.Stripe,
StripeSubscriptionId = subscription?.Id, GatewayCustomerId = customer?.Id,
GatewaySubscriptionId = subscription?.Id,
Enabled = true, Enabled = true,
CreationDate = DateTime.UtcNow, CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow RevisionDate = DateTime.UtcNow
@ -515,10 +516,10 @@ namespace Bit.Core.Services
public async Task DeleteAsync(Organization organization) public async Task DeleteAsync(Organization organization)
{ {
if(!string.IsNullOrWhiteSpace(organization.StripeSubscriptionId)) if(!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{ {
var subscriptionService = new StripeSubscriptionService(); var subscriptionService = new StripeSubscriptionService();
var canceledSub = await subscriptionService.CancelAsync(organization.StripeSubscriptionId, false); var canceledSub = await subscriptionService.CancelAsync(organization.GatewaySubscriptionId, false);
if(!canceledSub.CanceledAt.HasValue) if(!canceledSub.CanceledAt.HasValue)
{ {
throw new BadRequestException("Unable to cancel subscription."); throw new BadRequestException("Unable to cancel subscription.");
@ -559,10 +560,10 @@ namespace Bit.Core.Services
await _organizationRepository.ReplaceAsync(organization); await _organizationRepository.ReplaceAsync(organization);
if(updateBilling && !string.IsNullOrWhiteSpace(organization.StripeCustomerId)) if(updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{ {
var customerService = new StripeCustomerService(); var customerService = new StripeCustomerService();
await customerService.UpdateAsync(organization.StripeCustomerId, new StripeCustomerUpdateOptions await customerService.UpdateAsync(organization.GatewayCustomerId, new StripeCustomerUpdateOptions
{ {
Email = organization.BillingEmail, Email = organization.BillingEmail,
Description = organization.BusinessName Description = organization.BusinessName

View File

@ -60,8 +60,9 @@ namespace Bit.Core.Services
throw; throw;
} }
user.StripeCustomerId = customer.Id; user.Gateway = Enums.GatewayType.Stripe;
user.StripeSubscriptionId = subscription.Id; user.GatewayCustomerId = customer.Id;
user.GatewaySubscriptionId = subscription.Id;
} }
public async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, public async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage,
@ -69,7 +70,7 @@ namespace Bit.Core.Services
{ {
var subscriptionItemService = new StripeSubscriptionItemService(); var subscriptionItemService = new StripeSubscriptionItemService();
var subscriptionService = new StripeSubscriptionService(); var subscriptionService = new StripeSubscriptionService();
var sub = await subscriptionService.GetAsync(storableSubscriber.StripeSubscriptionId); var sub = await subscriptionService.GetAsync(storableSubscriber.GatewaySubscriptionId);
if(sub == null) if(sub == null)
{ {
throw new GatewayException("Subscription not found."); throw new GatewayException("Subscription not found.");
@ -108,13 +109,13 @@ namespace Bit.Core.Services
public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber) public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber)
{ {
if(!string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId)) if(!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
{ {
var subscriptionService = new StripeSubscriptionService(); var subscriptionService = new StripeSubscriptionService();
await subscriptionService.CancelAsync(subscriber.StripeSubscriptionId, false); await subscriptionService.CancelAsync(subscriber.GatewaySubscriptionId, false);
} }
if(string.IsNullOrWhiteSpace(subscriber.StripeCustomerId)) if(string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
return; return;
} }
@ -122,7 +123,7 @@ namespace Bit.Core.Services
var chargeService = new StripeChargeService(); var chargeService = new StripeChargeService();
var charges = await chargeService.ListAsync(new StripeChargeListOptions var charges = await chargeService.ListAsync(new StripeChargeListOptions
{ {
CustomerId = subscriber.StripeCustomerId CustomerId = subscriber.GatewayCustomerId
}); });
if(charges?.Data != null) if(charges?.Data != null)
@ -135,17 +136,17 @@ namespace Bit.Core.Services
} }
var customerService = new StripeCustomerService(); var customerService = new StripeCustomerService();
await customerService.DeleteAsync(subscriber.StripeCustomerId); await customerService.DeleteAsync(subscriber.GatewayCustomerId);
} }
public async Task PreviewUpcomingInvoiceAndPayAsync(ISubscriber subscriber, string planId, public async Task PreviewUpcomingInvoiceAndPayAsync(ISubscriber subscriber, string planId,
int prorateThreshold = 500) int prorateThreshold = 500)
{ {
var invoiceService = new StripeInvoiceService(); var invoiceService = new StripeInvoiceService();
var upcomingPreview = await invoiceService.UpcomingAsync(subscriber.StripeCustomerId, var upcomingPreview = await invoiceService.UpcomingAsync(subscriber.GatewayCustomerId,
new StripeUpcomingInvoiceOptions new StripeUpcomingInvoiceOptions
{ {
SubscriptionId = subscriber.StripeSubscriptionId SubscriptionId = subscriber.GatewaySubscriptionId
}); });
var prorationAmount = upcomingPreview.StripeInvoiceLineItems?.Data? var prorationAmount = upcomingPreview.StripeInvoiceLineItems?.Data?
@ -156,10 +157,10 @@ 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 month. // Invoice them and pay now instead of waiting until next month.
var invoice = await invoiceService.CreateAsync(subscriber.StripeCustomerId, var invoice = await invoiceService.CreateAsync(subscriber.GatewayCustomerId,
new StripeInvoiceCreateOptions new StripeInvoiceCreateOptions
{ {
SubscriptionId = subscriber.StripeSubscriptionId SubscriptionId = subscriber.GatewaySubscriptionId
}); });
if(invoice.AmountDue > 0) if(invoice.AmountDue > 0)
@ -178,13 +179,13 @@ namespace Bit.Core.Services
throw new ArgumentNullException(nameof(subscriber)); throw new ArgumentNullException(nameof(subscriber));
} }
if(string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId)) if(string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
{ {
throw new GatewayException("No subscription."); throw new GatewayException("No subscription.");
} }
var subscriptionService = new StripeSubscriptionService(); var subscriptionService = new StripeSubscriptionService();
var sub = await subscriptionService.GetAsync(subscriber.StripeSubscriptionId); var sub = await subscriptionService.GetAsync(subscriber.GatewaySubscriptionId);
if(sub == null) if(sub == null)
{ {
throw new GatewayException("Subscription was not found."); throw new GatewayException("Subscription was not found.");
@ -209,13 +210,13 @@ namespace Bit.Core.Services
throw new ArgumentNullException(nameof(subscriber)); throw new ArgumentNullException(nameof(subscriber));
} }
if(string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId)) if(string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
{ {
throw new GatewayException("No subscription."); throw new GatewayException("No subscription.");
} }
var subscriptionService = new StripeSubscriptionService(); var subscriptionService = new StripeSubscriptionService();
var sub = await subscriptionService.GetAsync(subscriber.StripeSubscriptionId); var sub = await subscriptionService.GetAsync(subscriber.GatewaySubscriptionId);
if(sub == null) if(sub == null)
{ {
throw new GatewayException("Subscription was not found."); throw new GatewayException("Subscription was not found.");
@ -241,15 +242,21 @@ namespace Bit.Core.Services
throw new ArgumentNullException(nameof(subscriber)); throw new ArgumentNullException(nameof(subscriber));
} }
if(subscriber.Gateway.HasValue && subscriber.Gateway.Value != Enums.GatewayType.Stripe)
{
throw new GatewayException("Switching from one payment type to another is not supported. " +
"Contact us for assistance.");
}
var updatedSubscriber = false; var updatedSubscriber = false;
var cardService = new StripeCardService(); var cardService = new StripeCardService();
var customerService = new StripeCustomerService(); var customerService = new StripeCustomerService();
StripeCustomer customer = null; StripeCustomer customer = null;
if(!string.IsNullOrWhiteSpace(subscriber.StripeCustomerId)) if(!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
customer = await customerService.GetAsync(subscriber.StripeCustomerId); customer = await customerService.GetAsync(subscriber.GatewayCustomerId);
} }
if(customer == null) if(customer == null)
@ -261,7 +268,8 @@ namespace Bit.Core.Services
SourceToken = paymentToken SourceToken = paymentToken
}); });
subscriber.StripeCustomerId = customer.Id; subscriber.Gateway = Enums.GatewayType.Stripe;
subscriber.GatewayCustomerId = customer.Id;
updatedSubscriber = true; updatedSubscriber = true;
} }
else else
@ -288,9 +296,9 @@ namespace Bit.Core.Services
var chargeService = new StripeChargeService(); var chargeService = new StripeChargeService();
var invoiceService = new StripeInvoiceService(); var invoiceService = new StripeInvoiceService();
if(!string.IsNullOrWhiteSpace(subscriber.StripeCustomerId)) if(!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
var customer = await customerService.GetAsync(subscriber.StripeCustomerId); var customer = await customerService.GetAsync(subscriber.GatewayCustomerId);
if(customer != null) if(customer != null)
{ {
if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId) && customer.Sources?.Data != null) if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId) && customer.Sources?.Data != null)
@ -324,19 +332,19 @@ namespace Bit.Core.Services
} }
} }
if(!string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId)) if(!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
{ {
var sub = await subscriptionService.GetAsync(subscriber.StripeSubscriptionId); var sub = await subscriptionService.GetAsync(subscriber.GatewaySubscriptionId);
if(sub != null) if(sub != null)
{ {
billingInfo.Subscription = new BillingInfo.BillingSubscription(sub); billingInfo.Subscription = new BillingInfo.BillingSubscription(sub);
} }
if(!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.StripeCustomerId)) if(!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
try try
{ {
var upcomingInvoice = await invoiceService.UpcomingAsync(subscriber.StripeCustomerId); var upcomingInvoice = await invoiceService.UpcomingAsync(subscriber.GatewayCustomerId);
if(upcomingInvoice != null) if(upcomingInvoice != null)
{ {
billingInfo.UpcomingInvoice = new BillingInfo.BillingInvoice(upcomingInvoice); billingInfo.UpcomingInvoice = new BillingInfo.BillingInvoice(upcomingInvoice);

View File

@ -164,10 +164,10 @@ namespace Bit.Core.Services
}); });
} }
if(!string.IsNullOrWhiteSpace(user.StripeSubscriptionId)) if(!string.IsNullOrWhiteSpace(user.GatewaySubscriptionId))
{ {
var subscriptionService = new StripeSubscriptionService(); var subscriptionService = new StripeSubscriptionService();
var canceledSub = await subscriptionService.CancelAsync(user.StripeSubscriptionId, false); var canceledSub = await subscriptionService.CancelAsync(user.GatewaySubscriptionId, false);
if(!canceledSub.CanceledAt.HasValue) if(!canceledSub.CanceledAt.HasValue)
{ {
throw new BadRequestException("Unable to cancel subscription."); throw new BadRequestException("Unable to cancel subscription.");
@ -560,7 +560,16 @@ namespace Bit.Core.Services
public async Task ReplacePaymentMethodAsync(User user, string paymentToken) public async Task ReplacePaymentMethodAsync(User user, string paymentToken)
{ {
var paymentService = user.GetPaymentService(_globalSettings); IPaymentService paymentService = null;
if(paymentToken.StartsWith("tok_"))
{
paymentService = new StripePaymentService();
}
else
{
paymentService = new BraintreePaymentService(_globalSettings);
}
var updated = await paymentService.UpdatePaymentMethodAsync(user, paymentToken); var updated = await paymentService.UpdatePaymentMethodAsync(user, paymentToken);
if(updated) if(updated)
{ {

View File

@ -16,12 +16,12 @@ namespace Bit.Core.Utilities
throw new ArgumentNullException(nameof(storableSubscriber)); throw new ArgumentNullException(nameof(storableSubscriber));
} }
if(string.IsNullOrWhiteSpace(storableSubscriber.StripeCustomerId)) if(string.IsNullOrWhiteSpace(storableSubscriber.GatewayCustomerId))
{ {
throw new BadRequestException("No payment method found."); throw new BadRequestException("No payment method found.");
} }
if(string.IsNullOrWhiteSpace(storableSubscriber.StripeSubscriptionId)) if(string.IsNullOrWhiteSpace(storableSubscriber.GatewaySubscriptionId))
{ {
throw new BadRequestException("No subscription found."); throw new BadRequestException("No subscription found.");
} }

View File

@ -205,4 +205,7 @@
<Build Include="dbo\Stored Procedures\User_UpdateStorage.sql" /> <Build Include="dbo\Stored Procedures\User_UpdateStorage.sql" />
<Build Include="dbo\Stored Procedures\Cipher_DeleteAttachment.sql" /> <Build Include="dbo\Stored Procedures\Cipher_DeleteAttachment.sql" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<RefactorLog Include="Sql.refactorlog" />
</ItemGroup>
</Project> </Project>

View File

@ -12,8 +12,9 @@
@UseTotp BIT, @UseTotp BIT,
@Storage BIGINT, @Storage BIGINT,
@MaxStorageGb SMALLINT, @MaxStorageGb SMALLINT,
@StripeCustomerId VARCHAR(50), @Gateway TINYINT,
@StripeSubscriptionId VARCHAR(50), @GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@Enabled BIT, @Enabled BIT,
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
@ -36,8 +37,9 @@ BEGIN
[UseTotp], [UseTotp],
[Storage], [Storage],
[MaxStorageGb], [MaxStorageGb],
[StripeCustomerId], [Gateway],
[StripeSubscriptionId], [GatewayCustomerId],
[GatewaySubscriptionId],
[Enabled], [Enabled],
[CreationDate], [CreationDate],
[RevisionDate] [RevisionDate]
@ -57,8 +59,9 @@ BEGIN
@UseTotp, @UseTotp,
@Storage, @Storage,
@MaxStorageGb, @MaxStorageGb,
@StripeCustomerId, @Gateway,
@StripeSubscriptionId, @GatewayCustomerId,
@GatewaySubscriptionId,
@Enabled, @Enabled,
@CreationDate, @CreationDate,
@RevisionDate @RevisionDate

View File

@ -12,8 +12,9 @@
@UseTotp BIT, @UseTotp BIT,
@Storage BIGINT, @Storage BIGINT,
@MaxStorageGb SMALLINT, @MaxStorageGb SMALLINT,
@StripeCustomerId VARCHAR(50), @Gateway TINYINT,
@StripeSubscriptionId VARCHAR(50), @GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@Enabled BIT, @Enabled BIT,
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
@ -37,8 +38,9 @@ BEGIN
[UseTotp] = @UseTotp, [UseTotp] = @UseTotp,
[Storage] = @Storage, [Storage] = @Storage,
[MaxStorageGb] = @MaxStorageGb, [MaxStorageGb] = @MaxStorageGb,
[StripeCustomerId] = @StripeCustomerId, [Gateway] = @Gateway,
[StripeSubscriptionId] = @StripeSubscriptionId, [GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId,
[Enabled] = @Enabled, [Enabled] = @Enabled,
[CreationDate] = @CreationDate, [CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate [RevisionDate] = @RevisionDate

View File

@ -18,8 +18,9 @@
@Premium BIT, @Premium BIT,
@Storage BIGINT, @Storage BIGINT,
@MaxStorageGb SMALLINT, @MaxStorageGb SMALLINT,
@StripeCustomerId VARCHAR(50), @Gateway TINYINT,
@StripeSubscriptionId VARCHAR(50), @GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
AS AS
@ -47,8 +48,9 @@ BEGIN
[Premium], [Premium],
[Storage], [Storage],
[MaxStorageGb], [MaxStorageGb],
[StripeCustomerId], [Gateway],
[StripeSubscriptionId], [GatewayCustomerId],
[GatewaySubscriptionId],
[CreationDate], [CreationDate],
[RevisionDate] [RevisionDate]
) )
@ -73,8 +75,9 @@ BEGIN
@Premium, @Premium,
@Storage, @Storage,
@MaxStorageGb, @MaxStorageGb,
@StripeCustomerId, @Gateway,
@StripeSubscriptionId, @GatewayCustomerId,
@GatewaySubscriptionId,
@CreationDate, @CreationDate,
@RevisionDate @RevisionDate
) )

View File

@ -18,8 +18,9 @@
@Premium BIT, @Premium BIT,
@Storage BIGINT, @Storage BIGINT,
@MaxStorageGb SMALLINT, @MaxStorageGb SMALLINT,
@StripeCustomerId VARCHAR(50), @Gateway TINYINT,
@StripeSubscriptionId VARCHAR(50), @GatewayCustomerId VARCHAR(50),
@GatewaySubscriptionId VARCHAR(50),
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7) @RevisionDate DATETIME2(7)
AS AS
@ -47,8 +48,9 @@ BEGIN
[Premium] = @Premium, [Premium] = @Premium,
[Storage] = @Storage, [Storage] = @Storage,
[MaxStorageGb] = @MaxStorageGb, [MaxStorageGb] = @MaxStorageGb,
[StripeCustomerId] = @StripeCustomerId, [Gateway] = @Gateway,
[StripeSubscriptionId] = @StripeSubscriptionId, [GatewayCustomerId] = @GatewayCustomerId,
[GatewaySubscriptionId] = @GatewaySubscriptionId,
[CreationDate] = @CreationDate, [CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate [RevisionDate] = @RevisionDate
WHERE WHERE

View File

@ -1,22 +1,23 @@
CREATE TABLE [dbo].[Organization] ( CREATE TABLE [dbo].[Organization] (
[Id] UNIQUEIDENTIFIER NOT NULL, [Id] UNIQUEIDENTIFIER NOT NULL,
[Name] NVARCHAR (50) NOT NULL, [Name] NVARCHAR (50) NOT NULL,
[BusinessName] NVARCHAR (50) NULL, [BusinessName] NVARCHAR (50) NULL,
[BillingEmail] NVARCHAR (50) NOT NULL, [BillingEmail] NVARCHAR (50) NOT NULL,
[Plan] NVARCHAR (50) NOT NULL, [Plan] NVARCHAR (50) NOT NULL,
[PlanType] TINYINT NOT NULL, [PlanType] TINYINT NOT NULL,
[Seats] SMALLINT NULL, [Seats] SMALLINT NULL,
[MaxCollections] SMALLINT NULL, [MaxCollections] SMALLINT NULL,
[UseGroups] BIT NOT NULL, [UseGroups] BIT NOT NULL,
[UseDirectory] BIT NOT NULL, [UseDirectory] BIT NOT NULL,
[UseTotp] BIT NOT NULL, [UseTotp] BIT NOT NULL,
[Storage] BIGINT NULL, [Storage] BIGINT NULL,
[MaxStorageGb] SMALLINT NULL, [MaxStorageGb] SMALLINT NULL,
[StripeCustomerId] VARCHAR (50) NULL, [Gateway] TINYINT NULL,
[StripeSubscriptionId] VARCHAR (50) NULL, [GatewayCustomerId] VARCHAR (50) NULL,
[Enabled] BIT NOT NULL, [GatewaySubscriptionId] VARCHAR (50) NULL,
[CreationDate] DATETIME2 (7) NOT NULL, [Enabled] BIT NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC) CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
); );

View File

@ -18,8 +18,9 @@
[Premium] BIT NOT NULL, [Premium] BIT NOT NULL,
[Storage] BIGINT NULL, [Storage] BIGINT NULL,
[MaxStorageGb] SMALLINT NULL, [MaxStorageGb] SMALLINT NULL,
[StripeCustomerId] VARCHAR (50) NULL, [Gateway] TINYINT NULL,
[StripeSubscriptionId] VARCHAR (50) NULL, [GatewayCustomerId] VARCHAR (50) NULL,
[GatewaySubscriptionId] VARCHAR (50) NULL,
[CreationDate] DATETIME2 (7) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC) CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)

View File

@ -0,0 +1,55 @@
EXEC sp_rename 'dbo.User.StripeSubscriptionId', 'GatewaySubscriptionId', 'COLUMN';
GO
EXEC sp_rename 'dbo.User.StripeCustomerId', 'GatewayCustomerId', 'COLUMN';
GO
EXEC sp_rename 'dbo.Organization.StripeSubscriptionId', 'GatewaySubscriptionId', 'COLUMN';
GO
EXEC sp_rename 'dbo.Organization.StripeCustomerId', 'GatewayCustomerId', 'COLUMN';
GO
alter table [user] add [Gateway] TINYINT NULL
go
alter table [organization] add [Gateway] TINYINT NULL
go
update [user] set [Gateway] = 0 where GatewaySubscriptionId IS NOT NULL
go
update [organization] set [Gateway] = 0 where GatewaySubscriptionId IS NOT NULL
go
drop view [dbo].[OrganizationView]
go
CREATE VIEW [dbo].[OrganizationView]
AS
SELECT
*
FROM
[dbo].[Organization]
GO
drop view [dbo].[UserView]
go
CREATE VIEW [dbo].[UserView]
AS
SELECT
*
FROM
[dbo].[User]
GO