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

APIs for premium. Billing helpers.

This commit is contained in:
Kyle Spearrin
2017-07-06 14:55:58 -04:00
parent 2afef85f85
commit d346ee5169
22 changed files with 789 additions and 313 deletions

View File

@ -10,7 +10,6 @@ using System.Collections.Generic;
using Microsoft.AspNetCore.DataProtection;
using Stripe;
using Bit.Core.Enums;
using Bit.Core.Models.StaticStore;
using Bit.Core.Models.Data;
namespace Bit.Core.Services
@ -48,66 +47,6 @@ namespace Bit.Core.Services
_pushNotificationService = pushNotificationService;
_pushRegistrationService = pushRegistrationService;
}
public async Task<OrganizationBilling> GetBillingAsync(Organization organization)
{
var orgBilling = new OrganizationBilling();
var customerService = new StripeCustomerService();
var subscriptionService = new StripeSubscriptionService();
var chargeService = new StripeChargeService();
var invoiceService = new StripeInvoiceService();
if(!string.IsNullOrWhiteSpace(organization.StripeCustomerId))
{
var customer = await customerService.GetAsync(organization.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(organization.StripeSubscriptionId))
{
var sub = await subscriptionService.GetAsync(organization.StripeSubscriptionId);
if(sub != null)
{
orgBilling.Subscription = sub;
}
if(!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(organization.StripeCustomerId))
{
try
{
var upcomingInvoice = await invoiceService.UpcomingAsync(organization.StripeCustomerId);
if(upcomingInvoice != null)
{
orgBilling.UpcomingInvoice = upcomingInvoice;
}
}
catch(StripeException) { }
}
}
return orgBilling;
}
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken)
{
@ -117,37 +56,11 @@ namespace Bit.Core.Services
throw new NotFoundException();
}
var cardService = new StripeCardService();
var customerService = new StripeCustomerService();
StripeCustomer customer = null;
if(!string.IsNullOrWhiteSpace(organization.StripeCustomerId))
var updated = await BillingHelpers.UpdatePaymentMethodAsync(organization, paymentToken);
if(updated)
{
customer = await customerService.GetAsync(organization.StripeCustomerId);
}
if(customer == null)
{
customer = await customerService.CreateAsync(new StripeCustomerCreateOptions
{
Description = organization.BusinessName,
Email = organization.BillingEmail,
SourceToken = paymentToken
});
organization.StripeCustomerId = customer.Id;
await _organizationRepository.ReplaceAsync(organization);
}
await cardService.CreateAsync(customer.Id, new StripeCardCreateOptions
{
SourceToken = paymentToken
});
if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId))
{
await cardService.DeleteAsync(customer.Id, customer.DefaultSourceId);
}
}
public async Task CancelSubscriptionAsync(Guid organizationId, bool endOfPeriod = false)
@ -158,28 +71,7 @@ namespace Bit.Core.Services
throw new NotFoundException();
}
if(string.IsNullOrWhiteSpace(organization.StripeSubscriptionId))
{
throw new BadRequestException("Organization has no subscription.");
}
var subscriptionService = new StripeSubscriptionService();
var sub = await subscriptionService.GetAsync(organization.StripeSubscriptionId);
if(sub == null)
{
throw new BadRequestException("Organization subscription was not found.");
}
if(sub.CanceledAt.HasValue)
{
throw new BadRequestException("Organization subscription is already canceled.");
}
var canceledSub = await subscriptionService.CancelAsync(sub.Id, endOfPeriod);
if(!canceledSub.CanceledAt.HasValue)
{
throw new BadRequestException("Unable to cancel subscription.");
}
await BillingHelpers.CancelSubscriptionAsync(organization, endOfPeriod);
}
public async Task ReinstateSubscriptionAsync(Guid organizationId)
@ -190,29 +82,7 @@ namespace Bit.Core.Services
throw new NotFoundException();
}
if(string.IsNullOrWhiteSpace(organization.StripeSubscriptionId))
{
throw new BadRequestException("Organization has no subscription.");
}
var subscriptionService = new StripeSubscriptionService();
var sub = await subscriptionService.GetAsync(organization.StripeSubscriptionId);
if(sub == null)
{
throw new BadRequestException("Organization subscription was not found.");
}
if(sub.Status != "active" || !sub.CanceledAt.HasValue)
{
throw new BadRequestException("Organization 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.");
}
await BillingHelpers.ReinstateSubscriptionAsync(organization);
}
public async Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats)
@ -427,8 +297,6 @@ namespace Bit.Core.Services
Prorate = true,
SubscriptionId = sub.Id
});
await PreviewUpcomingAndPayAsync(organization, plan);
}
else if(additionalSeats > 0)
{
@ -438,49 +306,21 @@ namespace Bit.Core.Services
Quantity = additionalSeats,
Prorate = true
});
await PreviewUpcomingAndPayAsync(organization, plan);
}
else if(additionalSeats == 0)
{
await subscriptionItemService.DeleteAsync(seatItem.Id);
}
if(additionalSeats > 0)
{
await BillingHelpers.PreviewUpcomingInvoiceAndPayAsync(organization, plan.StripeSeatPlanId, 500);
}
organization.Seats = (short?)newSeatTotal;
await _organizationRepository.ReplaceAsync(organization);
}
private async Task PreviewUpcomingAndPayAsync(Organization org, Plan plan)
{
var invoiceService = new StripeInvoiceService();
var upcomingPreview = await invoiceService.UpcomingAsync(org.StripeCustomerId,
new StripeUpcomingInvoiceOptions
{
SubscriptionId = org.StripeSubscriptionId
});
var prorationAmount = upcomingPreview.StripeInvoiceLineItems?.Data?
.TakeWhile(i => i.Plan.Id == plan.StripeSeatPlanId && i.Proration).Sum(i => i.Amount);
if(prorationAmount.GetValueOrDefault() >= 500)
{
try
{
// Owes more than $5.00 on next invoice. Invoice them and pay now instead of waiting until next month.
var invoice = await invoiceService.CreateAsync(org.StripeCustomerId,
new StripeInvoiceCreateOptions
{
SubscriptionId = org.StripeSubscriptionId
});
if(invoice.AmountDue > 0)
{
await invoiceService.PayAsync(invoice.Id);
}
}
catch(StripeException) { }
}
}
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup)
{
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan && !p.Disabled);
@ -620,27 +460,7 @@ namespace Bit.Core.Services
}
catch
{
if(subscription != null)
{
await subscriptionService.CancelAsync(subscription.Id, false);
}
if(customer != null)
{
var chargeService = new StripeChargeService();
var charges = await chargeService.ListAsync(new StripeChargeListOptions { CustomerId = customer.Id });
if(charges?.Data != null)
{
var refundService = new StripeRefundService();
foreach(var charge in charges.Data.Where(c => !c.Refunded))
{
await refundService.CreateAsync(charge.Id);
}
}
await customerService.DeleteAsync(customer.Id);
}
await BillingHelpers.CancelAndRecoverChargesAsync(subscription?.Id, customer?.Id);
if(organization.Id != default(Guid))
{
await _organizationRepository.DeleteAsync(organization);

View File

@ -16,11 +16,16 @@ using U2fLib = U2F.Core.Crypto.U2F;
using U2F.Core.Models;
using U2F.Core.Utils;
using Bit.Core.Exceptions;
using Stripe;
using Bit.Core.Utilities;
namespace Bit.Core.Services
{
public class UserService : UserManager<User>, IUserService, IDisposable
{
private const string PremiumPlanId = "premium-annually";
private const string StoragePlanId = "storage-gb-annually";
private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
@ -492,6 +497,109 @@ namespace Bit.Core.Services
return true;
}
public async Task SignUpPremiumAsync(User user, string paymentToken, short additionalStorageGb)
{
if(user.Premium)
{
throw new BadRequestException("Already a premium user.");
}
var customerService = new StripeCustomerService();
var customer = await customerService.CreateAsync(new StripeCustomerCreateOptions
{
Description = user.Name,
Email = user.Email,
SourceToken = paymentToken
});
var subCreateOptions = new StripeSubscriptionCreateOptions
{
Items = new List<StripeSubscriptionItemOption>(),
Metadata = new Dictionary<string, string>
{
["userId"] = user.Id.ToString()
}
};
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
{
PlanId = PremiumPlanId,
Quantity = 1
});
if(additionalStorageGb > 0)
{
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
{
PlanId = StoragePlanId,
Quantity = additionalStorageGb
});
}
StripeSubscription subscription = null;
try
{
var subscriptionService = new StripeSubscriptionService();
subscription = await subscriptionService.CreateAsync(customer.Id, subCreateOptions);
}
catch(StripeException)
{
await customerService.DeleteAsync(customer.Id);
throw;
}
user.Premium = true;
user.MaxStorageGb = (short)(1 + additionalStorageGb);
user.RevisionDate = DateTime.UtcNow;
user.StripeCustomerId = customer.Id;
user.StripeSubscriptionId = subscription.Id;
try
{
await SaveUserAsync(user);
}
catch
{
await BillingHelpers.CancelAndRecoverChargesAsync(subscription.Id, customer.Id);
throw;
}
}
public async Task AdjustStorageAsync(User user, short storageAdjustmentGb)
{
if(user == null)
{
throw new ArgumentNullException(nameof(user));
}
if(!user.Premium)
{
throw new BadRequestException("Not a premium user.");
}
await BillingHelpers.AdjustStorageAsync(user, storageAdjustmentGb, StoragePlanId);
await SaveUserAsync(user);
}
public async Task ReplacePaymentMethodAsync(User user, string paymentToken)
{
var updated = await BillingHelpers.UpdatePaymentMethodAsync(user, paymentToken);
if(updated)
{
await SaveUserAsync(user);
}
}
public async Task CancelPremiumAsync(User user, bool endOfPeriod = false)
{
await BillingHelpers.CancelSubscriptionAsync(user, endOfPeriod);
}
public async Task ReinstatePremiumAsync(User user)
{
await BillingHelpers.ReinstateSubscriptionAsync(user);
}
private async Task<IdentityResult> UpdatePasswordHash(User user, string newPassword, bool validatePassword = true)
{
if(validatePassword)