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

adjust storage with payment intent/method handling

This commit is contained in:
Kyle Spearrin 2019-08-10 12:59:32 -04:00
parent e60f1a4f50
commit 74bbeae776
9 changed files with 66 additions and 22 deletions

View File

@ -529,7 +529,7 @@ namespace Bit.Api.Controllers
[HttpPost("storage")] [HttpPost("storage")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task PostStorage([FromBody]StorageRequestModel model) public async Task<PaymentResponseModel> PostStorage([FromBody]StorageRequestModel model)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
if(user == null) if(user == null)
@ -537,7 +537,12 @@ namespace Bit.Api.Controllers
throw new UnauthorizedAccessException(); throw new UnauthorizedAccessException();
} }
await _userService.AdjustStorageAsync(user, model.StorageGbAdjustment.Value); var result = await _userService.AdjustStorageAsync(user, model.StorageGbAdjustment.Value);
return new PaymentResponseModel
{
Success = true,
PaymentIntentClientSecret = result
};
} }
[HttpPost("license")] [HttpPost("license")]

View File

@ -246,7 +246,7 @@ namespace Bit.Api.Controllers
[HttpPost("{id}/storage")] [HttpPost("{id}/storage")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task PostStorage(string id, [FromBody]StorageRequestModel model) public async Task<PaymentResponseModel> PostStorage(string id, [FromBody]StorageRequestModel model)
{ {
var orgIdGuid = new Guid(id); var orgIdGuid = new Guid(id);
if(!_currentContext.OrganizationOwner(orgIdGuid)) if(!_currentContext.OrganizationOwner(orgIdGuid))
@ -254,7 +254,12 @@ namespace Bit.Api.Controllers
throw new NotFoundException(); throw new NotFoundException();
} }
await _organizationService.AdjustStorageAsync(orgIdGuid, model.StorageGbAdjustment.Value); var result = await _organizationService.AdjustStorageAsync(orgIdGuid, model.StorageGbAdjustment.Value);
return new PaymentResponseModel
{
Success = true,
PaymentIntentClientSecret = result
};
} }
[HttpPost("{id}/verify-bank")] [HttpPost("{id}/verify-bank")]

View File

@ -14,7 +14,7 @@ namespace Bit.Core.Services
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null); Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
Task ReinstateSubscriptionAsync(Guid organizationId); Task ReinstateSubscriptionAsync(Guid organizationId);
Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade); Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade);
Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb); Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);
Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup organizationSignup); Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup organizationSignup);

View File

@ -15,7 +15,7 @@ namespace Bit.Core.Services
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon); short additionalStorageGb, short additionalSeats, bool premiumAccessAddon);
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
short additionalStorageGb); short additionalStorageGb);
Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId); Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId);
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false); Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);
Task ReinstateSubscriptionAsync(ISubscriber subscriber); Task ReinstateSubscriptionAsync(ISubscriber subscriber);
Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType,

View File

@ -46,7 +46,7 @@ namespace Bit.Core.Services
Task<Tuple<bool, string>> SignUpPremiumAsync(User user, string paymentToken, Task<Tuple<bool, string>> SignUpPremiumAsync(User user, string paymentToken,
PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license); PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license);
Task UpdateLicenseAsync(User user, UserLicense license); Task UpdateLicenseAsync(User user, UserLicense license);
Task AdjustStorageAsync(User user, short storageAdjustmentGb); Task<string> AdjustStorageAsync(User user, short storageAdjustmentGb);
Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType); Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType);
Task CancelPremiumAsync(User user, bool? endOfPeriod = null); Task CancelPremiumAsync(User user, bool? endOfPeriod = null);
Task ReinstatePremiumAsync(User user); Task ReinstatePremiumAsync(User user);

View File

@ -230,7 +230,7 @@ namespace Bit.Core.Services
return new Tuple<bool, string>(success, paymentIntentClientSecret); return new Tuple<bool, string>(success, paymentIntentClientSecret);
} }
public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb) public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
{ {
var organization = await GetOrgById(organizationId); var organization = await GetOrgById(organizationId);
if(organization == null) if(organization == null)
@ -249,9 +249,10 @@ namespace Bit.Core.Services
throw new BadRequestException("Plan does not allow additional storage."); throw new BadRequestException("Plan does not allow additional storage.");
} }
await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb, var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
plan.StripeStoragePlanId); plan.StripeStoragePlanId);
await ReplaceAndUpdateCache(organization); await ReplaceAndUpdateCache(organization);
return secret;
} }
public async Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment) public async Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment)
@ -374,8 +375,9 @@ namespace Bit.Core.Services
var invoicedNow = false; var invoicedNow = false;
if(additionalSeats > 0) if(additionalSeats > 0)
{ {
invoicedNow = await (_paymentService as StripePaymentService).PreviewUpcomingInvoiceAndPayAsync( var result = await (_paymentService as StripePaymentService).PreviewUpcomingInvoiceAndPayAsync(
organization, plan.StripeSeatPlanId, subItemOptions, 500); organization, plan.StripeSeatPlanId, subItemOptions, 500);
invoicedNow = result.Item1;
} }
await subUpdateAction(!invoicedNow); await subUpdateAction(!invoicedNow);

View File

@ -616,7 +616,7 @@ namespace Bit.Core.Services
}).ToList(); }).ToList();
} }
public async Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, public async Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage,
string storagePlanId) string storagePlanId)
{ {
var subscriptionItemService = new SubscriptionItemService(); var subscriptionItemService = new SubscriptionItemService();
@ -679,14 +679,18 @@ namespace Bit.Core.Services
subUpdateAction = (prorate) => subscriptionItemService.DeleteAsync(storageItem.Id); subUpdateAction = (prorate) => subscriptionItemService.DeleteAsync(storageItem.Id);
} }
string paymentIntentClientSecret = null;
var invoicedNow = false; var invoicedNow = false;
if(additionalStorage > 0) if(additionalStorage > 0)
{ {
invoicedNow = await PreviewUpcomingInvoiceAndPayAsync( var result = await PreviewUpcomingInvoiceAndPayAsync(
storableSubscriber, storagePlanId, subItemOptions, 400); storableSubscriber, storagePlanId, subItemOptions, 400);
invoicedNow = result.Item1;
paymentIntentClientSecret = result.Item2;
} }
await subUpdateAction(!invoicedNow); await subUpdateAction(!invoicedNow);
return paymentIntentClientSecret;
} }
public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber) public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber)
@ -748,11 +752,12 @@ namespace Bit.Core.Services
await customerService.DeleteAsync(subscriber.GatewayCustomerId); await customerService.DeleteAsync(subscriber.GatewayCustomerId);
} }
public async Task<bool> PreviewUpcomingInvoiceAndPayAsync(ISubscriber subscriber, string planId, public async Task<Tuple<bool, string>> PreviewUpcomingInvoiceAndPayAsync(ISubscriber subscriber, string planId,
List<InvoiceSubscriptionItemOptions> subItemOptions, int prorateThreshold = 500) List<InvoiceSubscriptionItemOptions> subItemOptions, int prorateThreshold = 500)
{ {
var invoiceService = new InvoiceService(); var invoiceService = new InvoiceService();
var invoiceItemService = new InvoiceItemService(); var invoiceItemService = new InvoiceItemService();
string paymentIntentClientSecret = null;
var pendingInvoiceItems = invoiceItemService.ListAutoPaging(new InvoiceItemListOptions var pendingInvoiceItems = invoiceItemService.ListAutoPaging(new InvoiceItemListOptions
{ {
@ -781,13 +786,18 @@ namespace Bit.Core.Services
customerOptions.AddExpand("default_source"); customerOptions.AddExpand("default_source");
var customer = await customerService.GetAsync(subscriber.GatewayCustomerId, customerOptions); var customer = await customerService.GetAsync(subscriber.GatewayCustomerId, customerOptions);
PaymentMethod cardPaymentMethod = null;
var invoiceAmountDue = upcomingPreview.StartingBalance + invoiceAmount; var invoiceAmountDue = upcomingPreview.StartingBalance + invoiceAmount;
if(invoiceAmountDue > 0 && !customer.Metadata.ContainsKey("btCustomerId")) if(invoiceAmountDue > 0 && !customer.Metadata.ContainsKey("btCustomerId"))
{ {
if(customer.DefaultSource == null || if(customer.DefaultSource == null ||
(!(customer.DefaultSource is Card) && !(customer.DefaultSource is BankAccount))) (!(customer.DefaultSource is Card) && !(customer.DefaultSource is BankAccount)))
{ {
throw new BadRequestException("No payment method is available."); cardPaymentMethod = GetDefaultCardPaymentMethod(customer.Id);
if(cardPaymentMethod == null)
{
throw new BadRequestException("No payment method is available.");
}
} }
} }
@ -819,7 +829,8 @@ namespace Bit.Core.Services
CollectionMethod = "send_invoice", CollectionMethod = "send_invoice",
DaysUntilDue = 1, DaysUntilDue = 1,
CustomerId = subscriber.GatewayCustomerId, CustomerId = subscriber.GatewayCustomerId,
SubscriptionId = subscriber.GatewaySubscriptionId SubscriptionId = subscriber.GatewaySubscriptionId,
DefaultPaymentMethodId = cardPaymentMethod?.Id
}); });
var invoicePayOptions = new InvoicePayOptions(); var invoicePayOptions = new InvoicePayOptions();
@ -864,15 +875,32 @@ namespace Bit.Core.Services
} }
}); });
} }
else
{
invoicePayOptions.OffSession = true;
invoicePayOptions.PaymentMethodId = cardPaymentMethod?.Id;
}
} }
try try
{ {
await invoiceService.PayAsync(invoice.Id, invoicePayOptions); await invoiceService.PayAsync(invoice.Id, invoicePayOptions);
} }
catch(StripeException) catch(StripeException e)
{ {
throw new GatewayException("Unable to pay invoice."); if(e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&
e.StripeError?.Code == "invoice_payment_intent_requires_action")
{
// SCA required, get intent client secret
var invoiceGetOptions = new InvoiceGetOptions();
invoiceGetOptions.AddExpand("payment_intent");
invoice = await invoiceService.GetAsync(invoice.Id, invoiceGetOptions);
paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret;
}
else
{
throw new GatewayException("Unable to pay invoice.");
}
} }
} }
catch(Exception e) catch(Exception e)
@ -926,7 +954,7 @@ namespace Bit.Core.Services
throw e; throw e;
} }
} }
return invoiceNow; return new Tuple<bool, string>(invoiceNow, paymentIntentClientSecret);
} }
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false) public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false)

View File

@ -774,7 +774,7 @@ namespace Bit.Core.Services
await SaveUserAsync(user); await SaveUserAsync(user);
} }
public async Task AdjustStorageAsync(User user, short storageAdjustmentGb) public async Task<string> AdjustStorageAsync(User user, short storageAdjustmentGb)
{ {
if(user == null) if(user == null)
{ {
@ -786,8 +786,10 @@ namespace Bit.Core.Services
throw new BadRequestException("Not a premium user."); throw new BadRequestException("Not a premium user.");
} }
await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, StoragePlanId); var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb,
StoragePlanId);
await SaveUserAsync(user); await SaveUserAsync(user);
return secret;
} }
public async Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType) public async Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType)

View File

@ -8,7 +8,7 @@ namespace Bit.Core.Utilities
{ {
public static class BillingHelpers public static class BillingHelpers
{ {
internal static async Task AdjustStorageAsync(IPaymentService paymentService, IStorableSubscriber storableSubscriber, internal static async Task<string> AdjustStorageAsync(IPaymentService paymentService, IStorableSubscriber storableSubscriber,
short storageAdjustmentGb, string storagePlanId) short storageAdjustmentGb, string storagePlanId)
{ {
if(storableSubscriber == null) if(storableSubscriber == null)
@ -51,8 +51,10 @@ namespace Bit.Core.Utilities
} }
var additionalStorage = newStorageGb - 1; var additionalStorage = newStorageGb - 1;
await paymentService.AdjustStorageAsync(storableSubscriber, additionalStorage, storagePlanId); var paymentIntentClientSecret = await paymentService.AdjustStorageAsync(storableSubscriber,
additionalStorage, storagePlanId);
storableSubscriber.MaxStorageGb = newStorageGb; storableSubscriber.MaxStorageGb = newStorageGb;
return paymentIntentClientSecret;
} }
} }
} }