mirror of
https://github.com/bitwarden/server.git
synced 2025-02-23 03:01:23 +01:00
refactor for addtnl. payment service (braintree)
This commit is contained in:
parent
2dc9c196c4
commit
082b53e133
@ -11,6 +11,7 @@ using Bit.Core.Enums;
|
||||
using System.Linq;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core;
|
||||
|
||||
namespace Bit.Api.Controllers
|
||||
{
|
||||
@ -22,17 +23,20 @@ namespace Bit.Api.Controllers
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public AccountsController(
|
||||
IUserService userService,
|
||||
ICipherService cipherService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
UserManager<User> userManager)
|
||||
UserManager<User> userManager,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_userService = userService;
|
||||
_cipherService = cipherService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_userManager = userManager;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
@ -363,7 +367,8 @@ namespace Bit.Api.Controllers
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var billingInfo = await BillingHelpers.GetBillingAsync(user);
|
||||
var paymentService = user.GetPaymentService(_globalSettings);
|
||||
var billingInfo = await paymentService.GetBillingAsync(user);
|
||||
if(billingInfo == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
|
@ -74,7 +74,8 @@ namespace Bit.Api.Controllers
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var billingInfo = await BillingHelpers.GetBillingAsync(organization);
|
||||
var paymentService = new StripePaymentService();
|
||||
var billingInfo = await paymentService.GetBillingAsync(organization);
|
||||
if(billingInfo == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
|
@ -44,6 +44,11 @@ namespace Bit.Api.Utilities
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
errorModel = new ErrorResponseModel(stripeException.StripeError.Parameter, stripeException.Message);
|
||||
}
|
||||
else if(exception is GatewayException)
|
||||
{
|
||||
errorModel.Message = exception.Message;
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
}
|
||||
else if(exception is ApplicationException)
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = 402;
|
||||
|
10
src/Core/Enums/PaymentMethodType.cs
Normal file
10
src/Core/Enums/PaymentMethodType.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum PaymentMethodType : byte
|
||||
{
|
||||
Card,
|
||||
BankAccount,
|
||||
PayPal,
|
||||
Bitcoin
|
||||
}
|
||||
}
|
11
src/Core/Exceptions/GatewayException.cs
Normal file
11
src/Core/Exceptions/GatewayException.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.Core.Exceptions
|
||||
{
|
||||
public class GatewayException : Exception
|
||||
{
|
||||
public GatewayException(string message, Exception innerException = null)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using Bit.Core.Models.Business;
|
||||
using Stripe;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Api
|
||||
{
|
||||
@ -32,47 +33,32 @@ namespace Bit.Core.Models.Api
|
||||
|
||||
public class BillingSource
|
||||
{
|
||||
public BillingSource(Source source)
|
||||
public BillingSource(BillingInfo.BillingSource source)
|
||||
{
|
||||
Type = source.Type;
|
||||
|
||||
switch(source.Type)
|
||||
{
|
||||
case SourceType.Card:
|
||||
Description = $"{source.Card.Brand}, *{source.Card.Last4}, " +
|
||||
string.Format("{0}/{1}",
|
||||
string.Concat(source.Card.ExpirationMonth.Length == 1 ?
|
||||
"0" : string.Empty, source.Card.ExpirationMonth),
|
||||
source.Card.ExpirationYear);
|
||||
CardBrand = source.Card.Brand;
|
||||
break;
|
||||
case SourceType.BankAccount:
|
||||
Description = $"{source.BankAccount.BankName}, *{source.BankAccount.Last4}";
|
||||
break;
|
||||
// bitcoin/alipay?
|
||||
default:
|
||||
break;
|
||||
}
|
||||
CardBrand = source.CardBrand;
|
||||
Description = source.Description;
|
||||
}
|
||||
|
||||
public SourceType Type { get; set; }
|
||||
public PaymentMethodType Type { get; set; }
|
||||
public string CardBrand { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
public class BillingSubscription
|
||||
{
|
||||
public BillingSubscription(StripeSubscription sub)
|
||||
public BillingSubscription(BillingInfo.BillingSubscription sub)
|
||||
{
|
||||
Status = sub.Status;
|
||||
TrialStartDate = sub.TrialStart;
|
||||
TrialEndDate = sub.TrialEnd;
|
||||
EndDate = sub.CurrentPeriodEnd;
|
||||
CancelledDate = sub.CanceledAt;
|
||||
CancelAtEndDate = sub.CancelAtPeriodEnd;
|
||||
if(sub.Items?.Data != null)
|
||||
TrialStartDate = sub.TrialStartDate;
|
||||
TrialEndDate = sub.TrialEndDate;
|
||||
EndDate = sub.EndDate;
|
||||
CancelledDate = sub.CancelledDate;
|
||||
CancelAtEndDate = sub.CancelAtEndDate;
|
||||
Cancelled = Cancelled;
|
||||
if(sub.Items != null)
|
||||
{
|
||||
Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i));
|
||||
Items = sub.Items.Select(i => new BillingSubscriptionItem(i));
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,19 +68,16 @@ namespace Bit.Core.Models.Api
|
||||
public DateTime? CancelledDate { get; set; }
|
||||
public bool CancelAtEndDate { get; set; }
|
||||
public string Status { get; set; }
|
||||
public bool Cancelled { get; set; }
|
||||
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
|
||||
|
||||
public class BillingSubscriptionItem
|
||||
{
|
||||
public BillingSubscriptionItem(StripeSubscriptionItem item)
|
||||
public BillingSubscriptionItem(BillingInfo.BillingSubscription.BillingSubscriptionItem item)
|
||||
{
|
||||
if(item.Plan != null)
|
||||
{
|
||||
Name = item.Plan.Name;
|
||||
Amount = item.Plan.Amount / 100M;
|
||||
Interval = item.Plan.Interval;
|
||||
}
|
||||
|
||||
Name = item.Name;
|
||||
Amount = item.Amount;
|
||||
Interval = item.Interval;
|
||||
Quantity = item.Quantity;
|
||||
}
|
||||
|
||||
@ -107,10 +90,10 @@ namespace Bit.Core.Models.Api
|
||||
|
||||
public class BillingInvoice
|
||||
{
|
||||
public BillingInvoice(StripeInvoice inv)
|
||||
public BillingInvoice(BillingInfo.BillingInvoice inv)
|
||||
{
|
||||
Amount = inv.AmountDue / 100M;
|
||||
Date = inv.Date.Value;
|
||||
Amount = inv.Amount;
|
||||
Date = inv.Date;
|
||||
}
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
@ -119,12 +102,12 @@ namespace Bit.Core.Models.Api
|
||||
|
||||
public class BillingCharge
|
||||
{
|
||||
public BillingCharge(StripeCharge charge)
|
||||
public BillingCharge(BillingInfo.BillingCharge charge)
|
||||
{
|
||||
Amount = charge.Amount / 100M;
|
||||
RefundedAmount = charge.AmountRefunded / 100M;
|
||||
PaymentSource = charge.Source != null ? new BillingSource(charge.Source) : null;
|
||||
CreatedDate = charge.Created;
|
||||
Amount = charge.Amount;
|
||||
RefundedAmount = charge.RefundedAmount;
|
||||
PaymentSource = charge.PaymentSource != null ? new BillingSource(charge.PaymentSource) : null;
|
||||
CreatedDate = charge.CreatedDate;
|
||||
FailureMessage = charge.FailureMessage;
|
||||
Refunded = charge.Refunded;
|
||||
Status = charge.Status;
|
||||
|
@ -1,13 +1,257 @@
|
||||
using Stripe;
|
||||
using Bit.Core.Enums;
|
||||
using Braintree;
|
||||
using Stripe;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.Core.Models.Business
|
||||
{
|
||||
public class BillingInfo
|
||||
{
|
||||
public Source PaymentSource { get; set; }
|
||||
public StripeSubscription Subscription { get; set; }
|
||||
public StripeInvoice UpcomingInvoice { get; set; }
|
||||
public IEnumerable<StripeCharge> Charges { get; set; } = new List<StripeCharge>();
|
||||
public BillingSource PaymentSource { get; set; }
|
||||
public BillingSubscription Subscription { get; set; }
|
||||
public BillingInvoice UpcomingInvoice { get; set; }
|
||||
public IEnumerable<BillingCharge> Charges { get; set; } = new List<BillingCharge>();
|
||||
|
||||
public class BillingSource
|
||||
{
|
||||
public BillingSource(Source source)
|
||||
{
|
||||
switch(source.Type)
|
||||
{
|
||||
case SourceType.Card:
|
||||
Type = PaymentMethodType.Card;
|
||||
Description = $"{source.Card.Brand}, *{source.Card.Last4}, " +
|
||||
string.Format("{0}/{1}",
|
||||
string.Concat(source.Card.ExpirationMonth.Length == 1 ?
|
||||
"0" : string.Empty, source.Card.ExpirationMonth),
|
||||
source.Card.ExpirationYear);
|
||||
CardBrand = source.Card.Brand;
|
||||
break;
|
||||
case SourceType.BankAccount:
|
||||
Type = PaymentMethodType.BankAccount;
|
||||
Description = $"{source.BankAccount.BankName}, *{source.BankAccount.Last4}";
|
||||
break;
|
||||
// bitcoin?
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public BillingSource(PaymentMethod method)
|
||||
{
|
||||
if(method is PayPalAccount paypal)
|
||||
{
|
||||
Type = PaymentMethodType.PayPal;
|
||||
Description = paypal.Email;
|
||||
}
|
||||
else if(method is CreditCard card)
|
||||
{
|
||||
Type = PaymentMethodType.Card;
|
||||
Description = $"{card.CardType.ToString()}, *{card.LastFour}, " +
|
||||
string.Format("{0}/{1}",
|
||||
string.Concat(card.ExpirationMonth.Length == 1 ?
|
||||
"0" : string.Empty, card.ExpirationMonth),
|
||||
card.ExpirationYear);
|
||||
CardBrand = card.CardType.ToString();
|
||||
}
|
||||
else if(method is UsBankAccount bank)
|
||||
{
|
||||
Type = PaymentMethodType.BankAccount;
|
||||
Description = $"{bank.BankName}, *{bank.Last4}";
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Method not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
public BillingSource(UsBankAccountDetails bank)
|
||||
{
|
||||
Type = PaymentMethodType.BankAccount;
|
||||
Description = $"{bank.BankName}, *{bank.Last4}";
|
||||
}
|
||||
|
||||
public BillingSource(PayPalDetails paypal)
|
||||
{
|
||||
Type = PaymentMethodType.PayPal;
|
||||
Description = paypal.PayerEmail;
|
||||
}
|
||||
|
||||
public PaymentMethodType Type { get; set; }
|
||||
public string CardBrand { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
public class BillingSubscription
|
||||
{
|
||||
public BillingSubscription(StripeSubscription sub)
|
||||
{
|
||||
Status = sub.Status;
|
||||
TrialStartDate = sub.TrialStart;
|
||||
TrialEndDate = sub.TrialEnd;
|
||||
EndDate = sub.CurrentPeriodEnd;
|
||||
CancelledDate = sub.CanceledAt;
|
||||
CancelAtEndDate = sub.CancelAtPeriodEnd;
|
||||
Cancelled = sub.Status == "cancelled";
|
||||
if(sub.Items?.Data != null)
|
||||
{
|
||||
Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i));
|
||||
}
|
||||
}
|
||||
|
||||
public BillingSubscription(Subscription sub, Plan plan)
|
||||
{
|
||||
Status = sub.Status.ToString();
|
||||
|
||||
if(sub.HasTrialPeriod.GetValueOrDefault() && sub.CreatedAt.HasValue && sub.TrialDuration.HasValue)
|
||||
{
|
||||
TrialStartDate = sub.CreatedAt.Value;
|
||||
if(sub.TrialDurationUnit == SubscriptionDurationUnit.DAY)
|
||||
{
|
||||
TrialEndDate = TrialStartDate.Value.AddDays(sub.TrialDuration.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
TrialEndDate = TrialStartDate.Value.AddMonths(sub.TrialDuration.Value);
|
||||
}
|
||||
}
|
||||
|
||||
EndDate = sub.BillingPeriodEndDate;
|
||||
|
||||
CancelAtEndDate = !sub.NeverExpires.GetValueOrDefault();
|
||||
Cancelled = sub.Status == SubscriptionStatus.CANCELED;
|
||||
if(Cancelled)
|
||||
{
|
||||
CancelledDate = sub.UpdatedAt.Value;
|
||||
}
|
||||
|
||||
var items = new List<BillingSubscriptionItem>();
|
||||
items.Add(new BillingSubscriptionItem(plan));
|
||||
if(sub.AddOns != null)
|
||||
{
|
||||
items.AddRange(sub.AddOns.Select(a => new BillingSubscriptionItem(plan, a)));
|
||||
}
|
||||
|
||||
if(items.Count > 0)
|
||||
{
|
||||
Items = items;
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? TrialStartDate { get; set; }
|
||||
public DateTime? TrialEndDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
public DateTime? CancelledDate { get; set; }
|
||||
public bool CancelAtEndDate { get; set; }
|
||||
public string Status { get; set; }
|
||||
public bool Cancelled { get; set; }
|
||||
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
|
||||
|
||||
public class BillingSubscriptionItem
|
||||
{
|
||||
public BillingSubscriptionItem(StripeSubscriptionItem item)
|
||||
{
|
||||
if(item.Plan != null)
|
||||
{
|
||||
Name = item.Plan.Name;
|
||||
Amount = item.Plan.Amount / 100M;
|
||||
Interval = item.Plan.Interval;
|
||||
}
|
||||
|
||||
Quantity = item.Quantity;
|
||||
}
|
||||
|
||||
public BillingSubscriptionItem(Plan plan)
|
||||
{
|
||||
Name = plan.Name;
|
||||
Amount = plan.Price.GetValueOrDefault();
|
||||
Interval = plan.BillingFrequency.GetValueOrDefault() == 12 ? "year" : "month";
|
||||
Quantity = 1;
|
||||
}
|
||||
|
||||
public BillingSubscriptionItem(Plan plan, AddOn addon)
|
||||
{
|
||||
Name = addon.Name;
|
||||
Amount = addon.Amount.GetValueOrDefault();
|
||||
Interval = plan.BillingFrequency.GetValueOrDefault() == 12 ? "year" : "month";
|
||||
Quantity = addon.Quantity.GetValueOrDefault();
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
public string Interval { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public class BillingInvoice
|
||||
{
|
||||
public BillingInvoice(StripeInvoice inv)
|
||||
{
|
||||
Amount = inv.AmountDue / 100M;
|
||||
Date = inv.Date.Value;
|
||||
}
|
||||
|
||||
public BillingInvoice(Subscription sub)
|
||||
{
|
||||
Amount = sub.NextBillAmount.GetValueOrDefault();
|
||||
Date = sub.NextBillingDate;
|
||||
}
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
public DateTime? Date { get; set; }
|
||||
}
|
||||
|
||||
public class BillingCharge
|
||||
{
|
||||
public BillingCharge(StripeCharge charge)
|
||||
{
|
||||
Amount = charge.Amount / 100M;
|
||||
RefundedAmount = charge.AmountRefunded / 100M;
|
||||
PaymentSource = charge.Source != null ? new BillingSource(charge.Source) : null;
|
||||
CreatedDate = charge.Created;
|
||||
FailureMessage = charge.FailureMessage;
|
||||
Refunded = charge.Refunded;
|
||||
Status = charge.Status;
|
||||
InvoiceId = charge.InvoiceId;
|
||||
}
|
||||
|
||||
public BillingCharge(Transaction transaction)
|
||||
{
|
||||
Amount = transaction.Amount.GetValueOrDefault();
|
||||
RefundedAmount = 0; // TODO?
|
||||
|
||||
if(transaction.PayPalDetails != null)
|
||||
{
|
||||
PaymentSource = new BillingSource(transaction.PayPalDetails);
|
||||
}
|
||||
else if(transaction.CreditCard != null && transaction.CreditCard.CardType != CreditCardCardType.UNRECOGNIZED)
|
||||
{
|
||||
PaymentSource = new BillingSource(transaction.CreditCard);
|
||||
}
|
||||
else if(transaction.UsBankAccountDetails != null)
|
||||
{
|
||||
PaymentSource = new BillingSource(transaction.UsBankAccountDetails);
|
||||
}
|
||||
|
||||
CreatedDate = transaction.CreatedAt.GetValueOrDefault();
|
||||
FailureMessage = null;
|
||||
Refunded = transaction.RefundedTransactionId != null;
|
||||
Status = transaction.Status.ToString();
|
||||
InvoiceId = null;
|
||||
}
|
||||
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public BillingSource PaymentSource { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string FailureMessage { get; set; }
|
||||
public bool Refunded { get; set; }
|
||||
public bool PartiallyRefunded => !Refunded && RefundedAmount > 0;
|
||||
public decimal RefundedAmount { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using Bit.Core.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using System.Linq;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Models.Table
|
||||
{
|
||||
@ -135,5 +136,20 @@ namespace Bit.Core.Models.Table
|
||||
|
||||
return maxStorageBytes - Storage.Value;
|
||||
}
|
||||
|
||||
public IPaymentService GetPaymentService(GlobalSettings globalSettings)
|
||||
{
|
||||
IPaymentService paymentService = null;
|
||||
if(StripeSubscriptionId.StartsWith("sub_"))
|
||||
{
|
||||
paymentService = new StripePaymentService();
|
||||
}
|
||||
else
|
||||
{
|
||||
paymentService = new BraintreePaymentService(globalSettings);
|
||||
}
|
||||
|
||||
return paymentService;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,17 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public interface IPaymentService
|
||||
{
|
||||
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
||||
Task PurchasePremiumAsync(User user, string paymentToken, short additionalStorageGb);
|
||||
Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId);
|
||||
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);
|
||||
Task ReinstateSubscriptionAsync(ISubscriber subscriber);
|
||||
Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, string paymentToken);
|
||||
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -1,139 +1,15 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Table;
|
||||
using Stripe;
|
||||
using Bit.Core.Services;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
public static class BillingHelpers
|
||||
{
|
||||
internal static async Task CancelAndRecoverChargesAsync(string subscriptionId, string customerId)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(subscriptionId))
|
||||
{
|
||||
var subscriptionService = new StripeSubscriptionService();
|
||||
await subscriptionService.CancelAsync(subscriptionId, false);
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(customerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var chargeService = new StripeChargeService();
|
||||
var charges = await chargeService.ListAsync(new StripeChargeListOptions { CustomerId = customerId });
|
||||
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(customerId);
|
||||
}
|
||||
|
||||
public static async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber)
|
||||
{
|
||||
var orgBilling = new BillingInfo();
|
||||
var customerService = new StripeCustomerService();
|
||||
var subscriptionService = new StripeSubscriptionService();
|
||||
var chargeService = new StripeChargeService();
|
||||
var invoiceService = new StripeInvoiceService();
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
|
||||
{
|
||||
var customer = await customerService.GetAsync(subscriber.StripeCustomerId);
|
||||
if(customer != null)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId) && customer.Sources?.Data != null)
|
||||
{
|
||||
if(customer.DefaultSourceId.StartsWith("card_"))
|
||||
{
|
||||
orgBilling.PaymentSource =
|
||||
customer.Sources.Data.FirstOrDefault(s => s.Card?.Id == customer.DefaultSourceId);
|
||||
}
|
||||
else if(customer.DefaultSourceId.StartsWith("ba_"))
|
||||
{
|
||||
orgBilling.PaymentSource =
|
||||
customer.Sources.Data.FirstOrDefault(s => s.BankAccount?.Id == customer.DefaultSourceId);
|
||||
}
|
||||
}
|
||||
|
||||
var charges = await chargeService.ListAsync(new StripeChargeListOptions
|
||||
{
|
||||
CustomerId = customer.Id,
|
||||
Limit = 20
|
||||
});
|
||||
orgBilling.Charges = charges?.Data?.OrderByDescending(c => c.Created);
|
||||
}
|
||||
}
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
|
||||
{
|
||||
var sub = await subscriptionService.GetAsync(subscriber.StripeSubscriptionId);
|
||||
if(sub != null)
|
||||
{
|
||||
orgBilling.Subscription = sub;
|
||||
}
|
||||
|
||||
if(!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.StripeCustomerId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var upcomingInvoice = await invoiceService.UpcomingAsync(subscriber.StripeCustomerId);
|
||||
if(upcomingInvoice != null)
|
||||
{
|
||||
orgBilling.UpcomingInvoice = upcomingInvoice;
|
||||
}
|
||||
}
|
||||
catch(StripeException) { }
|
||||
}
|
||||
}
|
||||
|
||||
return orgBilling;
|
||||
}
|
||||
|
||||
internal static 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) { }
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, short storageAdjustmentGb,
|
||||
string storagePlanId)
|
||||
internal static async Task AdjustStorageAsync(IPaymentService paymentService, IStorableSubscriber storableSubscriber,
|
||||
short storageAdjustmentGb, string storagePlanId)
|
||||
{
|
||||
if(storableSubscriber == null)
|
||||
{
|
||||
@ -175,152 +51,8 @@ namespace Bit.Core.Utilities
|
||||
}
|
||||
|
||||
var additionalStorage = newStorageGb - 1;
|
||||
var subscriptionItemService = new StripeSubscriptionItemService();
|
||||
var subscriptionService = new StripeSubscriptionService();
|
||||
var sub = await subscriptionService.GetAsync(storableSubscriber.StripeSubscriptionId);
|
||||
if(sub == null)
|
||||
{
|
||||
throw new BadRequestException("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);
|
||||
}
|
||||
|
||||
await paymentService.AdjustStorageAsync(storableSubscriber, additionalStorage, storagePlanId);
|
||||
storableSubscriber.MaxStorageGb = newStorageGb;
|
||||
}
|
||||
|
||||
public static 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;
|
||||
}
|
||||
|
||||
await cardService.CreateAsync(customer.Id, new StripeCardCreateOptions
|
||||
{
|
||||
SourceToken = paymentToken
|
||||
});
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId))
|
||||
{
|
||||
await cardService.DeleteAsync(customer.Id, customer.DefaultSourceId);
|
||||
}
|
||||
|
||||
return updatedSubscriber;
|
||||
}
|
||||
|
||||
public static async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false)
|
||||
{
|
||||
if(subscriber == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(subscriber));
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
|
||||
{
|
||||
throw new BadRequestException("No subscription.");
|
||||
}
|
||||
|
||||
var subscriptionService = new StripeSubscriptionService();
|
||||
var sub = await subscriptionService.GetAsync(subscriber.StripeSubscriptionId);
|
||||
if(sub == null)
|
||||
{
|
||||
throw new BadRequestException("Subscription was not found.");
|
||||
}
|
||||
|
||||
if(sub.CanceledAt.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Subscription is already canceled.");
|
||||
}
|
||||
|
||||
var canceledSub = await subscriptionService.CancelAsync(sub.Id, endOfPeriod);
|
||||
if(!canceledSub.CanceledAt.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Unable to cancel subscription.");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ReinstateSubscriptionAsync(ISubscriber subscriber)
|
||||
{
|
||||
if(subscriber == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(subscriber));
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(subscriber.StripeSubscriptionId))
|
||||
{
|
||||
throw new BadRequestException("No subscription.");
|
||||
}
|
||||
|
||||
var subscriptionService = new StripeSubscriptionService();
|
||||
var sub = await subscriptionService.GetAsync(subscriber.StripeSubscriptionId);
|
||||
if(sub == null)
|
||||
{
|
||||
throw new BadRequestException("Subscription was not found.");
|
||||
}
|
||||
|
||||
if((sub.Status != "active" && sub.Status != "trialing") || !sub.CanceledAt.HasValue)
|
||||
{
|
||||
throw new BadRequestException("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 BadRequestException("Unable to reinstate subscription.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user