1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36: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

@ -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 CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
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 AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
@ -22,6 +22,7 @@ namespace Bit.Core.Services
string ownerKey, string collectionName);
Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license);
Task DeleteAsync(Organization organization);
Task EnableAsync(Guid organizationId, DateTime? expirationDate);
Task DisableAsync(Guid organizationId, DateTime? expirationDate);
Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);
Task EnableAsync(Guid organizationId);

View File

@ -10,9 +10,9 @@ namespace Bit.Core.Services
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, string paymentToken,
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);
Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
short additionalStorageGb);
Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId);
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, string token);
Task SendDeleteConfirmationAsync(string email);
Task SignUpPremiumAsync(User user, string paymentToken, PaymentMethodType paymentMethodType,
short additionalStorageGb, UserLicense license);
Task<Tuple<bool, string>> SignUpPremiumAsync(User user, string paymentToken,
PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license);
Task UpdateLicenseAsync(User user, UserLicense license);
Task AdjustStorageAsync(User user, short storageAdjustmentGb);
Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType);
Task CancelPremiumAsync(User user, bool? endOfPeriod = null);
Task ReinstatePremiumAsync(User user);
Task EnablePremiumAsync(Guid userId, DateTime? expirationDate);
Task EnablePremiumAsync(User user, DateTime? expirationDate);
Task DisablePremiumAsync(Guid userId, DateTime? expirationDate);
Task DisablePremiumAsync(User user, DateTime? expirationDate);
Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate);

View File

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

View File

@ -161,7 +161,7 @@ namespace Bit.Core.Services
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)
{
if(!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
@ -231,28 +231,53 @@ namespace Bit.Core.Services
{
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;
stripePaymentMethod = true;
if(customer.DefaultSource is Card || customer.DefaultSource is SourceCard)
{
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;
stripePaymentMethod = true;
var paymentMethod = GetDefaultCardPaymentMethod(customer.Id);
if(paymentMethod != null)
{
paymentMethodType = PaymentMethodType.Card;
stripePaymentMethod = true;
subCreateOptions.DefaultPaymentMethodId = paymentMethod.Id;
}
}
}
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
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,
short additionalStorageGb)
public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType,
string paymentToken, short additionalStorageGb)
{
if(paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken))
{
@ -384,8 +409,18 @@ namespace Bit.Core.Services
user.Gateway = GatewayType.Stripe;
user.GatewayCustomerId = customer.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,
@ -481,10 +516,6 @@ namespace Bit.Core.Services
await subscriptionService.CancelAsync(subscription.Id, new SubscriptionCancelOptions());
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())
@ -1237,10 +1268,7 @@ namespace Bit.Core.Services
}
if(billingInfo.PaymentSource == null)
{
var paymentMethodService = new PaymentMethodService();
var cardPaymentMethods = paymentMethodService.ListAutoPaging(
new PaymentMethodListOptions { CustomerId = customer.Id, Type = "card" });
var paymentMethod = cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
var paymentMethod = GetDefaultCardPaymentMethod(customer.Id);
if(paymentMethod != null)
{
billingInfo.PaymentSource = new BillingInfo.BillingSource(paymentMethod);
@ -1292,5 +1320,13 @@ namespace Bit.Core.Services
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;
}
public async Task SignUpPremiumAsync(User user, string paymentToken, PaymentMethodType paymentMethodType,
short additionalStorageGb, UserLicense license)
public async Task<Tuple<bool, string>> SignUpPremiumAsync(User user, string paymentToken,
PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license)
{
if(user.Premium)
{
@ -692,6 +692,7 @@ namespace Bit.Core.Services
throw new BadRequestException("You can't subtract storage!");
}
string paymentIntentClientSecret = null;
IPaymentService paymentService = null;
if(_globalSettings.SelfHosted)
{
@ -711,7 +712,8 @@ namespace Bit.Core.Services
}
else
{
await _paymentService.PurchasePremiumAsync(user, paymentMethodType, paymentToken, additionalStorageGb);
paymentIntentClientSecret = await _paymentService.PurchasePremiumAsync(user, paymentMethodType,
paymentToken, additionalStorageGb);
}
user.Premium = true;
@ -739,6 +741,8 @@ namespace Bit.Core.Services
await paymentService.CancelAndRecoverChargesAsync(user);
throw;
}
return new Tuple<bool, string>(string.IsNullOrWhiteSpace(paymentIntentClientSecret),
paymentIntentClientSecret);
}
public async Task UpdateLicenseAsync(User user, UserLicense license)
@ -816,6 +820,23 @@ namespace Bit.Core.Services
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)
{
var user = await _userRepository.GetByIdAsync(userId);