1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-21 13:05:11 -05:00

upgrade org api

This commit is contained in:
Kyle Spearrin 2019-03-21 21:36:03 -04:00
parent 81788b3eb0
commit 5bfed59f9c
8 changed files with 252 additions and 133 deletions

View File

@ -223,7 +223,7 @@ namespace Bit.Api.Controllers
throw new NotFoundException(); throw new NotFoundException();
} }
await _organizationService.UpgradePlanAsync(orgIdGuid, model.PlanType, model.AdditionalSeats); await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
} }
[HttpPost("{id}/seat")] [HttpPost("{id}/seat")]

View File

@ -1,12 +1,30 @@
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api namespace Bit.Core.Models.Api
{ {
public class OrganizationUpgradeRequestModel public class OrganizationUpgradeRequestModel
{ {
[StringLength(50)]
public string BusinessName { get; set; }
public PlanType PlanType { get; set; } public PlanType PlanType { get; set; }
[Range(0, double.MaxValue)] [Range(0, double.MaxValue)]
public short AdditionalSeats { get; set; } public short AdditionalSeats { get; set; }
[Range(0, 99)]
public short? AdditionalStorageGb { get; set; }
public bool PremiumAccessAddon { get; set; }
public OrganizationUpgrade ToOrganizationUpgrade()
{
return new OrganizationUpgrade
{
AdditionalSeats = AdditionalSeats,
AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(),
BusinessName = BusinessName,
Plan = PlanType,
PremiumAccessAddon = PremiumAccessAddon
};
}
} }
} }

View File

@ -3,19 +3,14 @@ using Bit.Core.Models.Table;
namespace Bit.Core.Models.Business namespace Bit.Core.Models.Business
{ {
public class OrganizationSignup public class OrganizationSignup : OrganizationUpgrade
{ {
public string Name { get; set; } public string Name { get; set; }
public string BusinessName { get; set; }
public string BillingEmail { get; set; } public string BillingEmail { get; set; }
public User Owner { get; set; } public User Owner { get; set; }
public string OwnerKey { get; set; } public string OwnerKey { get; set; }
public PlanType Plan { get; set; } public string CollectionName { get; set; }
public short AdditionalSeats { get; set; }
public short AdditionalStorageGb { get; set; }
public bool PremiumAccessAddon { get; set; }
public PaymentMethodType? PaymentMethodType { get; set; } public PaymentMethodType? PaymentMethodType { get; set; }
public string PaymentToken { get; set; } public string PaymentToken { get; set; }
public string CollectionName { get; set; }
} }
} }

View File

@ -0,0 +1,13 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.Business
{
public class OrganizationUpgrade
{
public string BusinessName { get; set; }
public PlanType Plan { get; set; }
public short AdditionalSeats { get; set; }
public short AdditionalStorageGb { get; set; }
public bool PremiumAccessAddon { 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, PlanType plan, int additionalSeats); Task 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);

View File

@ -10,6 +10,8 @@ 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,
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon);
Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, Task 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);

View File

@ -117,7 +117,7 @@ namespace Bit.Core.Services
await _paymentService.ReinstateSubscriptionAsync(organization); await _paymentService.ReinstateSubscriptionAsync(organization);
} }
public async Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats) public async Task UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
{ {
var organization = await GetOrgById(organizationId); var organization = await GetOrgById(organizationId);
if(organization == null) if(organization == null)
@ -127,7 +127,7 @@ namespace Bit.Core.Services
if(string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) if(string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{ {
throw new BadRequestException("No payment method found."); throw new BadRequestException("Your account has no payment method available.");
} }
var existingPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); var existingPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
@ -136,7 +136,7 @@ namespace Bit.Core.Services
throw new BadRequestException("Existing plan not found."); throw new BadRequestException("Existing plan not found.");
} }
var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == plan && !p.Disabled); var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
if(newPlan == null) if(newPlan == null)
{ {
throw new BadRequestException("Plan not found."); throw new BadRequestException("Plan not found.");
@ -152,31 +152,27 @@ namespace Bit.Core.Services
throw new BadRequestException("You cannot upgrade to this plan."); throw new BadRequestException("You cannot upgrade to this plan.");
} }
if(!newPlan.CanBuyAdditionalSeats && additionalSeats > 0) if(existingPlan.Type != PlanType.Free)
{ {
throw new BadRequestException("Plan does not allow additional seats."); throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
} }
if(newPlan.CanBuyAdditionalSeats && newPlan.MaxAdditionalSeats.HasValue && ValidateOrganizationUpgradeParameters(newPlan, upgrade);
additionalSeats > newPlan.MaxAdditionalSeats.Value)
{
throw new BadRequestException($"Selected plan allows a maximum of " +
$"{newPlan.MaxAdditionalSeats.Value} additional seats.");
}
var newPlanSeats = (short)(newPlan.BaseSeats + (newPlan.CanBuyAdditionalSeats ? additionalSeats : 0)); var newPlanSeats = (short)(newPlan.BaseSeats +
(newPlan.CanBuyAdditionalSeats ? upgrade.AdditionalSeats : 0));
if(!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats) if(!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
{ {
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id); var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
if(userCount > newPlanSeats) if(userCount > newPlanSeats)
{ {
throw new BadRequestException($"Your organization currently has {userCount} seats filled. Your new plan " + throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
$"only has ({newPlanSeats}) seats. Remove some users."); $"Your new plan only has ({newPlanSeats}) seats. Remove some users.");
} }
} }
if(newPlan.MaxCollections.HasValue && if(newPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
(!organization.MaxCollections.HasValue || organization.MaxCollections.Value > newPlan.MaxCollections.Value)) organization.MaxCollections.Value > newPlan.MaxCollections.Value))
{ {
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id); var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
if(collectionCount > newPlan.MaxCollections.Value) if(collectionCount > newPlan.MaxCollections.Value)
@ -187,72 +183,47 @@ namespace Bit.Core.Services
} }
} }
// TODO: Groups? if(!newPlan.UseGroups && organization.UseGroups)
{
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
if(groups.Any())
{
throw new BadRequestException($"Your new plan does not allow the groups feature. " +
$"Remove your groups.");
}
}
// TODO: Check storage?
var subscriptionService = new Stripe.SubscriptionService();
if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) if(string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{ {
// They must have been on a free plan. Create new sub. await _paymentService.UpgradeFreeOrganizationAsync(organization, newPlan,
var subCreateOptions = new SubscriptionCreateOptions upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon);
{
CustomerId = organization.GatewayCustomerId,
TrialPeriodDays = newPlan.TrialPeriodDays,
Items = new List<SubscriptionItemOption>(),
Metadata = new Dictionary<string, string> {
{ "organizationId", organization.Id.ToString() }
}
};
if(newPlan.StripePlanId != null)
{
subCreateOptions.Items.Add(new SubscriptionItemOption
{
PlanId = newPlan.StripePlanId,
Quantity = 1
});
}
if(additionalSeats > 0 && newPlan.StripeSeatPlanId != null)
{
subCreateOptions.Items.Add(new SubscriptionItemOption
{
PlanId = newPlan.StripeSeatPlanId,
Quantity = additionalSeats
});
}
await subscriptionService.CreateAsync(subCreateOptions);
} }
else else
{ {
// Update existing sub. // TODO: Update existing sub
var subUpdateOptions = new SubscriptionUpdateOptions throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
{
Items = new List<SubscriptionItemUpdateOption>()
};
if(newPlan.StripePlanId != null)
{
subUpdateOptions.Items.Add(new SubscriptionItemUpdateOption
{
PlanId = newPlan.StripePlanId,
Quantity = 1
});
} }
if(additionalSeats > 0 && newPlan.StripeSeatPlanId != null) organization.BusinessName = upgrade.BusinessName;
{ organization.PlanType = newPlan.Type;
subUpdateOptions.Items.Add(new SubscriptionItemUpdateOption organization.Seats = (short)(newPlan.BaseSeats + upgrade.AdditionalSeats);
{ organization.MaxCollections = newPlan.MaxCollections;
PlanId = newPlan.StripeSeatPlanId, organization.MaxStorageGb = !newPlan.MaxStorageGb.HasValue ?
Quantity = additionalSeats (short?)null : (short)(newPlan.MaxStorageGb.Value + upgrade.AdditionalStorageGb);
}); organization.UseGroups = newPlan.UseGroups;
} organization.UseDirectory = newPlan.UseDirectory;
organization.UseEvents = newPlan.UseEvents;
await subscriptionService.UpdateAsync(organization.GatewaySubscriptionId, subUpdateOptions); organization.UseTotp = newPlan.UseTotp;
} organization.Use2fa = newPlan.Use2fa;
organization.UseApi = newPlan.UseApi;
// TODO: Update organization organization.SelfHost = newPlan.SelfHost;
organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
organization.Plan = newPlan.Name;
organization.Enabled = true;
organization.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(organization);
} }
public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb) public async Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
@ -459,42 +430,7 @@ namespace Bit.Core.Services
throw new BadRequestException("Plan not found."); throw new BadRequestException("Plan not found.");
} }
if(!plan.MaxStorageGb.HasValue && signup.AdditionalStorageGb > 0) ValidateOrganizationUpgradeParameters(plan, signup);
{
throw new BadRequestException("Plan does not allow additional storage.");
}
if(signup.AdditionalStorageGb < 0)
{
throw new BadRequestException("You can't subtract storage!");
}
if(!plan.CanBuyPremiumAccessAddon && signup.PremiumAccessAddon)
{
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
}
if(plan.BaseSeats + signup.AdditionalSeats <= 0)
{
throw new BadRequestException("You do not have any seats!");
}
if(signup.AdditionalSeats < 0)
{
throw new BadRequestException("You can't subtract seats!");
}
if(!plan.CanBuyAdditionalSeats && signup.AdditionalSeats > 0)
{
throw new BadRequestException("Plan does not allow additional users.");
}
if(plan.CanBuyAdditionalSeats && plan.MaxAdditionalSeats.HasValue &&
signup.AdditionalSeats > plan.MaxAdditionalSeats.Value)
{
throw new BadRequestException($"Selected plan allows a maximum of " +
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
}
var organization = new Organization var organization = new Organization
{ {
@ -644,7 +580,8 @@ namespace Bit.Core.Services
// push // push
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value); var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds, organization.Id.ToString()); await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds,
organization.Id.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(ownerId); await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
return new Tuple<Organization, OrganizationUser>(organization, orgUser); return new Tuple<Organization, OrganizationUser>(organization, orgUser);
@ -1395,5 +1332,45 @@ namespace Bit.Core.Services
{ {
return await _organizationRepository.GetByIdAsync(id); return await _organizationRepository.GetByIdAsync(id);
} }
private void ValidateOrganizationUpgradeParameters(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
{
if(!plan.MaxStorageGb.HasValue && upgrade.AdditionalStorageGb > 0)
{
throw new BadRequestException("Plan does not allow additional storage.");
}
if(upgrade.AdditionalStorageGb < 0)
{
throw new BadRequestException("You can't subtract storage!");
}
if(!plan.CanBuyPremiumAccessAddon && upgrade.PremiumAccessAddon)
{
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
}
if(plan.BaseSeats + upgrade.AdditionalSeats <= 0)
{
throw new BadRequestException("You do not have any seats!");
}
if(upgrade.AdditionalSeats < 0)
{
throw new BadRequestException("You can't subtract seats!");
}
if(!plan.CanBuyAdditionalSeats && upgrade.AdditionalSeats > 0)
{
throw new BadRequestException("Plan does not allow additional users.");
}
if(plan.CanBuyAdditionalSeats && plan.MaxAdditionalSeats.HasValue &&
upgrade.AdditionalSeats > plan.MaxAdditionalSeats.Value)
{
throw new BadRequestException($"Selected plan allows a maximum of " +
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
}
}
} }
} }

View File

@ -162,6 +162,95 @@ namespace Bit.Core.Services
org.ExpirationDate = subscription.CurrentPeriodEnd; org.ExpirationDate = subscription.CurrentPeriodEnd;
} }
public async Task UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan,
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon)
{
if(!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
{
throw new BadRequestException("Organization already has a subscription.");
}
var customerService = new CustomerService();
customerService.ExpandDefaultSource = true;
var customer = await customerService.GetAsync(org.GatewayCustomerId);
if(customer == null)
{
throw new GatewayException("Could not find customer payment profile.");
}
var subCreateOptions = new SubscriptionCreateOptions
{
CustomerId = customer.Id,
Items = new List<SubscriptionItemOption>(),
Metadata = new Dictionary<string, string>
{
[org.GatewayIdField()] = org.Id.ToString()
}
};
if(plan.StripePlanId != null)
{
subCreateOptions.Items.Add(new SubscriptionItemOption
{
PlanId = plan.StripePlanId,
Quantity = 1
});
}
if(additionalSeats > 0 && plan.StripeSeatPlanId != null)
{
subCreateOptions.Items.Add(new SubscriptionItemOption
{
PlanId = plan.StripeSeatPlanId,
Quantity = additionalSeats
});
}
if(additionalStorageGb > 0)
{
subCreateOptions.Items.Add(new SubscriptionItemOption
{
PlanId = plan.StripeStoragePlanId,
Quantity = additionalStorageGb
});
}
if(premiumAccessAddon && plan.StripePremiumAccessPlanId != null)
{
subCreateOptions.Items.Add(new SubscriptionItemOption
{
PlanId = plan.StripePremiumAccessPlanId,
Quantity = 1
});
}
var stripePaymentMethod = false;
var paymentMethodType = PaymentMethodType.Credit;
var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId");
if(hasBtCustomerId)
{
paymentMethodType = PaymentMethodType.PayPal;
}
if(!hasBtCustomerId && customer.DefaultSource != null)
{
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;
}
}
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
org.GatewaySubscriptionId = subscription.Id;
org.ExpirationDate = subscription.CurrentPeriodEnd;
}
public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
short additionalStorageGb) short additionalStorageGb)
{ {
@ -179,13 +268,9 @@ namespace Bit.Core.Services
throw new GatewayException("Bank account payment method is not supported at this time."); throw new GatewayException("Bank account payment method is not supported at this time.");
} }
var invoiceService = new InvoiceService();
var customerService = new CustomerService(); var customerService = new CustomerService();
var createdStripeCustomer = false; var createdStripeCustomer = false;
var addedCreditToStripeCustomer = false;
Customer customer = null; Customer customer = null;
Braintree.Transaction braintreeTransaction = null;
Braintree.Customer braintreeCustomer = null; Braintree.Customer braintreeCustomer = null;
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card || var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.Credit; paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.Credit;
@ -283,6 +368,25 @@ namespace Bit.Core.Services
}); });
} }
var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer,
stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer);
user.Gateway = GatewayType.Stripe;
user.GatewayCustomerId = customer.Id;
user.GatewaySubscriptionId = subscription.Id;
user.Premium = true;
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
}
private async Task<Subscription> ChargeForNewSubscriptionAsync(ISubscriber subcriber, Customer customer,
bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType,
SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
{
var addedCreditToStripeCustomer = false;
Braintree.Transaction braintreeTransaction = null;
var invoiceService = new InvoiceService();
var customerService = new CustomerService();
var subInvoiceMetadata = new Dictionary<string, string>(); var subInvoiceMetadata = new Dictionary<string, string>();
Subscription subscription = null; Subscription subscription = null;
try try
@ -312,12 +416,12 @@ namespace Bit.Core.Services
SubmitForSettlement = true, SubmitForSettlement = true,
PayPal = new Braintree.TransactionOptionsPayPalRequest PayPal = new Braintree.TransactionOptionsPayPalRequest
{ {
CustomField = $"{user.BraintreeIdField()}:{user.Id}" CustomField = $"{subcriber.BraintreeIdField()}:{subcriber.Id}"
} }
}, },
CustomFields = new Dictionary<string, string> CustomFields = new Dictionary<string, string>
{ {
[user.BraintreeIdField()] = user.Id.ToString() [subcriber.BraintreeIdField()] = subcriber.Id.ToString()
} }
}); });
@ -377,6 +481,8 @@ namespace Bit.Core.Services
Metadata = subInvoiceMetadata Metadata = subInvoiceMetadata
}); });
} }
return subscription;
} }
catch(Exception e) catch(Exception e)
{ {
@ -386,7 +492,7 @@ namespace Bit.Core.Services
{ {
await customerService.DeleteAsync(customer.Id); await customerService.DeleteAsync(customer.Id);
} }
else if(addedCreditToStripeCustomer) else if(addedCreditToStripeCustomer || customer.AccountBalance < 0)
{ {
await customerService.UpdateAsync(customer.Id, new CustomerUpdateOptions await customerService.UpdateAsync(customer.Id, new CustomerUpdateOptions
{ {
@ -402,14 +508,15 @@ namespace Bit.Core.Services
{ {
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
} }
throw e;
if(e is StripeException strEx &&
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
{
throw new GatewayException("Bank account is not yet verified.");
} }
user.Gateway = GatewayType.Stripe; throw e;
user.GatewayCustomerId = customer.Id; }
user.GatewaySubscriptionId = subscription.Id;
user.Premium = true;
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
} }
private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions( private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
@ -721,6 +828,13 @@ namespace Bit.Core.Services
await invoiceItemService.DeleteAsync(ii.Id); await invoiceItemService.DeleteAsync(ii.Id);
} }
} }
if(e is StripeException strEx &&
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
{
throw new GatewayException("Bank account is not yet verified.");
}
throw e; throw e;
} }
} }