1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-08 06:28:14 -05:00

payment intent/method support for incomplete status

This commit is contained in:
Kyle Spearrin 2019-08-09 23:56:26 -04:00
parent efcf626999
commit 00e808d731
10 changed files with 173 additions and 39 deletions

View File

@ -443,7 +443,7 @@ namespace Bit.Api.Controllers
} }
[HttpPost("premium")] [HttpPost("premium")]
public async Task<ProfileResponseModel> PostPremium(PremiumRequestModel model) public async Task<PaymentResponseModel> PostPremium(PremiumRequestModel model)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
if(user == null) if(user == null)
@ -463,9 +463,15 @@ namespace Bit.Api.Controllers
throw new BadRequestException("Invalid license."); throw new BadRequestException("Invalid license.");
} }
await _userService.SignUpPremiumAsync(user, model.PaymentToken, model.PaymentMethodType.Value, var result = await _userService.SignUpPremiumAsync(user, model.PaymentToken,
model.AdditionalStorageGb.GetValueOrDefault(0), license); model.PaymentMethodType.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license);
return new ProfileResponseModel(user, null, await _userService.TwoFactorIsEnabledAsync(user)); var profile = new ProfileResponseModel(user, null, await _userService.TwoFactorIsEnabledAsync(user));
return new PaymentResponseModel
{
UserProfile = profile,
PaymentIntentClientSecret = result.Item2,
Success = result.Item1
};
} }
[HttpGet("billing")] [HttpGet("billing")]

View File

@ -215,7 +215,7 @@ namespace Bit.Api.Controllers
[HttpPost("{id}/upgrade")] [HttpPost("{id}/upgrade")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task PostUpgrade(string id, [FromBody]OrganizationUpgradeRequestModel model) public async Task<PaymentResponseModel> PostUpgrade(string id, [FromBody]OrganizationUpgradeRequestModel model)
{ {
var orgIdGuid = new Guid(id); var orgIdGuid = new Guid(id);
if(!_currentContext.OrganizationOwner(orgIdGuid)) if(!_currentContext.OrganizationOwner(orgIdGuid))
@ -223,7 +223,12 @@ namespace Bit.Api.Controllers
throw new NotFoundException(); throw new NotFoundException();
} }
await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade()); var result = await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
return new PaymentResponseModel
{
Success = result.Item1,
PaymentIntentClientSecret = result.Item2
};
} }
[HttpPost("{id}/seat")] [HttpPost("{id}/seat")]

View File

@ -3,6 +3,7 @@ using Bit.Core.Enums;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -338,6 +339,39 @@ namespace Bit.Billing.Controllers
_logger.LogWarning("Charge refund amount doesn't seem correct. " + charge.Id); _logger.LogWarning("Charge refund amount doesn't seem correct. " + charge.Id);
} }
} }
else if(parsedEvent.Type.Equals("invoice.payment_succeeded"))
{
if(!(parsedEvent.Data.Object is Invoice invoice))
{
throw new Exception("Invoice is null. " + parsedEvent.Id);
}
if(invoice.Paid && invoice.BillingReason == "subscription_create")
{
var subscriptionService = new SubscriptionService();
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
if(subscription?.Status == "active")
{
var ids = GetIdsFromMetaData(subscription.Metadata);
// org
if(ids.Item1.HasValue)
{
if(subscription.Items.Any(i => StaticStore.Plans.Any(p => p.StripePlanId == i.Plan.Id)))
{
await _organizationService.EnableAsync(ids.Item1.Value, subscription.CurrentPeriodEnd);
}
}
// user
else if(ids.Item2.HasValue)
{
if(subscription.Items.Any(i => i.Plan.Id == "premium-annually"))
{
await _userService.EnablePremiumAsync(ids.Item2.Value, subscription.CurrentPeriodEnd);
}
}
}
}
}
else if(parsedEvent.Type.Equals("invoice.payment_failed")) else if(parsedEvent.Type.Equals("invoice.payment_failed"))
{ {
if(!(parsedEvent.Data.Object is Invoice invoice)) if(!(parsedEvent.Data.Object is Invoice invoice))

View File

@ -0,0 +1,13 @@
namespace Bit.Core.Models.Api
{
public class PaymentResponseModel : ResponseModel
{
public PaymentResponseModel()
: base("payment")
{ }
public ProfileResponseModel UserProfile { get; set; }
public string PaymentIntentClientSecret { get; set; }
public bool Success { get; set; }
}
}

View File

@ -13,7 +13,7 @@ namespace Bit.Core.Services
Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, PaymentMethodType paymentMethodType); Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, PaymentMethodType paymentMethodType);
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null); Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
Task ReinstateSubscriptionAsync(Guid organizationId); Task ReinstateSubscriptionAsync(Guid organizationId);
Task UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade); Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade);
Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb); Task 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);
@ -22,6 +22,7 @@ namespace Bit.Core.Services
string ownerKey, string collectionName); string ownerKey, string collectionName);
Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license); Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license);
Task DeleteAsync(Organization organization); Task DeleteAsync(Organization organization);
Task EnableAsync(Guid organizationId, DateTime? expirationDate);
Task DisableAsync(Guid organizationId, DateTime? expirationDate); Task DisableAsync(Guid organizationId, DateTime? expirationDate);
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);
Task EnableAsync(Guid organizationId); Task EnableAsync(Guid organizationId);

View File

@ -10,9 +10,9 @@ namespace Bit.Core.Services
Task CancelAndRecoverChargesAsync(ISubscriber subscriber); Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, string paymentToken, Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, string paymentToken,
Models.StaticStore.Plan plan, short additionalStorageGb, short additionalSeats, bool premiumAccessAddon); Models.StaticStore.Plan plan, short additionalStorageGb, short additionalSeats, bool premiumAccessAddon);
Task UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan, Task<string> UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan,
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon); short additionalStorageGb, short additionalSeats, bool premiumAccessAddon);
Task 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 AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId);
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false); Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);

View File

@ -43,13 +43,15 @@ namespace Bit.Core.Services
Task<IdentityResult> DeleteAsync(User user); Task<IdentityResult> DeleteAsync(User user);
Task<IdentityResult> DeleteAsync(User user, string token); Task<IdentityResult> DeleteAsync(User user, string token);
Task SendDeleteConfirmationAsync(string email); Task SendDeleteConfirmationAsync(string email);
Task SignUpPremiumAsync(User user, string paymentToken, PaymentMethodType paymentMethodType, Task<Tuple<bool, string>> SignUpPremiumAsync(User user, string paymentToken,
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 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);
Task EnablePremiumAsync(Guid userId, DateTime? expirationDate);
Task EnablePremiumAsync(User user, DateTime? expirationDate);
Task DisablePremiumAsync(Guid userId, DateTime? expirationDate); Task DisablePremiumAsync(Guid userId, DateTime? expirationDate);
Task DisablePremiumAsync(User user, DateTime? expirationDate); Task DisablePremiumAsync(User user, DateTime? expirationDate);
Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate); Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate);

View File

@ -117,7 +117,7 @@ namespace Bit.Core.Services
await _paymentService.ReinstateSubscriptionAsync(organization); await _paymentService.ReinstateSubscriptionAsync(organization);
} }
public async Task UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade) public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
{ {
var organization = await GetOrgById(organizationId); var organization = await GetOrgById(organizationId);
if(organization == null) if(organization == null)
@ -195,10 +195,13 @@ namespace Bit.Core.Services
// TODO: Check storage? // TODO: Check storage?
string paymentIntentClientSecret = null;
var success = true;
if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{ {
await _paymentService.UpgradeFreeOrganizationAsync(organization, newPlan, paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization, newPlan,
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon); upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon);
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
} }
else else
{ {
@ -221,9 +224,10 @@ namespace Bit.Core.Services
organization.SelfHost = newPlan.SelfHost; organization.SelfHost = newPlan.SelfHost;
organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon; organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
organization.Plan = newPlan.Name; organization.Plan = newPlan.Name;
organization.Enabled = true; organization.Enabled = success;
organization.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(organization); await ReplaceAndUpdateCache(organization);
return new Tuple<bool, string>(success, paymentIntentClientSecret);
} }
public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb) public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
@ -710,6 +714,18 @@ namespace Bit.Core.Services
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
} }
public async Task EnableAsync(Guid organizationId, DateTime? expirationDate)
{
var org = await GetOrgById(organizationId);
if(org != null && !org.Enabled)
{
org.Enabled = true;
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(org);
}
}
public async Task DisableAsync(Guid organizationId, DateTime? expirationDate) public async Task DisableAsync(Guid organizationId, DateTime? expirationDate)
{ {
var org = await GetOrgById(organizationId); var org = await GetOrgById(organizationId);

View File

@ -161,7 +161,7 @@ namespace Bit.Core.Services
org.ExpirationDate = subscription.CurrentPeriodEnd; org.ExpirationDate = subscription.CurrentPeriodEnd;
} }
public async Task UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan, public async Task<string> UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan,
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon) short additionalStorageGb, short additionalSeats, bool premiumAccessAddon)
{ {
if(!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId)) if(!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
@ -231,28 +231,53 @@ namespace Bit.Core.Services
{ {
paymentMethodType = PaymentMethodType.PayPal; paymentMethodType = PaymentMethodType.PayPal;
} }
if(!hasBtCustomerId && customer.DefaultSource != null) else
{ {
if(customer.DefaultSource is Card || customer.DefaultSource is SourceCard) if(customer.DefaultSource != null)
{ {
paymentMethodType = PaymentMethodType.Card; if(customer.DefaultSource is Card || customer.DefaultSource is SourceCard)
stripePaymentMethod = true; {
paymentMethodType = PaymentMethodType.Card;
stripePaymentMethod = true;
}
else if(customer.DefaultSource is BankAccount || customer.DefaultSource is SourceAchDebit)
{
paymentMethodType = PaymentMethodType.BankAccount;
stripePaymentMethod = true;
}
} }
else if(customer.DefaultSource is BankAccount || customer.DefaultSource is SourceAchDebit) else
{ {
paymentMethodType = PaymentMethodType.BankAccount; var paymentMethod = GetDefaultCardPaymentMethod(customer.Id);
stripePaymentMethod = true; if(paymentMethod != null)
{
paymentMethodType = PaymentMethodType.Card;
stripePaymentMethod = true;
subCreateOptions.DefaultPaymentMethodId = paymentMethod.Id;
}
} }
} }
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false, var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
stripePaymentMethod, paymentMethodType, subCreateOptions, null); stripePaymentMethod, paymentMethodType, subCreateOptions, null);
org.GatewaySubscriptionId = subscription.Id; org.GatewaySubscriptionId = subscription.Id;
org.ExpirationDate = subscription.CurrentPeriodEnd;
if(subscription.Status == "incomplete" &&
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
{
org.Enabled = false;
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
}
else
{
org.Enabled = true;
org.ExpirationDate = subscription.CurrentPeriodEnd;
return null;
}
} }
public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType,
short additionalStorageGb) string paymentToken, short additionalStorageGb)
{ {
if(paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken)) if(paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken))
{ {
@ -384,8 +409,18 @@ namespace Bit.Core.Services
user.Gateway = GatewayType.Stripe; user.Gateway = GatewayType.Stripe;
user.GatewayCustomerId = customer.Id; user.GatewayCustomerId = customer.Id;
user.GatewaySubscriptionId = subscription.Id; user.GatewaySubscriptionId = subscription.Id;
user.Premium = true;
user.PremiumExpirationDate = subscription.CurrentPeriodEnd; if(subscription.Status == "incomplete" &&
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
{
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
}
else
{
user.Premium = true;
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
return null;
}
} }
private async Task<Subscription> ChargeForNewSubscriptionAsync(ISubscriber subcriber, Customer customer, private async Task<Subscription> ChargeForNewSubscriptionAsync(ISubscriber subcriber, Customer customer,
@ -481,10 +516,6 @@ namespace Bit.Core.Services
await subscriptionService.CancelAsync(subscription.Id, new SubscriptionCancelOptions()); await subscriptionService.CancelAsync(subscription.Id, new SubscriptionCancelOptions());
throw new GatewayException("Payment method failed."); throw new GatewayException("Payment method failed.");
} }
else if(subscription.LatestInvoice.PaymentIntent.Status == "requires_action")
{
// Needs SCA. Send email? Should be handled by Stripe.
}
} }
if(!stripePaymentMethod && subInvoiceMetadata.Any()) if(!stripePaymentMethod && subInvoiceMetadata.Any())
@ -1237,10 +1268,7 @@ namespace Bit.Core.Services
} }
if(billingInfo.PaymentSource == null) if(billingInfo.PaymentSource == null)
{ {
var paymentMethodService = new PaymentMethodService(); var paymentMethod = GetDefaultCardPaymentMethod(customer.Id);
var cardPaymentMethods = paymentMethodService.ListAutoPaging(
new PaymentMethodListOptions { CustomerId = customer.Id, Type = "card" });
var paymentMethod = cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
if(paymentMethod != null) if(paymentMethod != null)
{ {
billingInfo.PaymentSource = new BillingInfo.BillingSource(paymentMethod); billingInfo.PaymentSource = new BillingInfo.BillingSource(paymentMethod);
@ -1292,5 +1320,13 @@ namespace Bit.Core.Services
return subscriptionInfo; return subscriptionInfo;
} }
private PaymentMethod GetDefaultCardPaymentMethod(string customerId)
{
var paymentMethodService = new PaymentMethodService();
var cardPaymentMethods = paymentMethodService.ListAutoPaging(
new PaymentMethodListOptions { CustomerId = customerId, Type = "card" });
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
}
} }
} }

View File

@ -679,8 +679,8 @@ namespace Bit.Core.Services
return true; return true;
} }
public async Task SignUpPremiumAsync(User user, string paymentToken, PaymentMethodType paymentMethodType, public async Task<Tuple<bool, string>> SignUpPremiumAsync(User user, string paymentToken,
short additionalStorageGb, UserLicense license) PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license)
{ {
if(user.Premium) if(user.Premium)
{ {
@ -692,6 +692,7 @@ namespace Bit.Core.Services
throw new BadRequestException("You can't subtract storage!"); throw new BadRequestException("You can't subtract storage!");
} }
string paymentIntentClientSecret = null;
IPaymentService paymentService = null; IPaymentService paymentService = null;
if(_globalSettings.SelfHosted) if(_globalSettings.SelfHosted)
{ {
@ -711,7 +712,8 @@ namespace Bit.Core.Services
} }
else else
{ {
await _paymentService.PurchasePremiumAsync(user, paymentMethodType, paymentToken, additionalStorageGb); paymentIntentClientSecret = await _paymentService.PurchasePremiumAsync(user, paymentMethodType,
paymentToken, additionalStorageGb);
} }
user.Premium = true; user.Premium = true;
@ -739,6 +741,8 @@ namespace Bit.Core.Services
await paymentService.CancelAndRecoverChargesAsync(user); await paymentService.CancelAndRecoverChargesAsync(user);
throw; throw;
} }
return new Tuple<bool, string>(string.IsNullOrWhiteSpace(paymentIntentClientSecret),
paymentIntentClientSecret);
} }
public async Task UpdateLicenseAsync(User user, UserLicense license) public async Task UpdateLicenseAsync(User user, UserLicense license)
@ -816,6 +820,23 @@ namespace Bit.Core.Services
await _paymentService.ReinstateSubscriptionAsync(user); await _paymentService.ReinstateSubscriptionAsync(user);
} }
public async Task EnablePremiumAsync(Guid userId, DateTime? expirationDate)
{
var user = await _userRepository.GetByIdAsync(userId);
await EnablePremiumAsync(user, expirationDate);
}
public async Task EnablePremiumAsync(User user, DateTime? expirationDate)
{
if(user != null && !user.Premium)
{
user.Premium = true;
user.PremiumExpirationDate = expirationDate;
user.RevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
}
}
public async Task DisablePremiumAsync(Guid userId, DateTime? expirationDate) public async Task DisablePremiumAsync(Guid userId, DateTime? expirationDate)
{ {
var user = await _userRepository.GetByIdAsync(userId); var user = await _userRepository.GetByIdAsync(userId);