1
0
mirror of https://github.com/bitwarden/server synced 2025-12-25 20:53:16 +00:00

[AC-1503] Fix Stripe integration on organization upgrade (#3084)

* Fix SM parameters not being passed to Stripe

* Fix flaky test

* Fix error message
This commit is contained in:
Thomas Rittson
2023-07-11 19:07:57 +10:00
committed by GitHub
parent a5efec301e
commit cab23cb109
8 changed files with 56 additions and 35 deletions

View File

@@ -7,7 +7,7 @@ namespace Bit.Core.Models.Business;
public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions
{
public OrganizationSubscriptionOptionsBase(Organization org, List<StaticStore.Plan> plans, TaxInfo taxInfo, int additionalSeats,
int additionalStorageGb, bool premiumAccessAddon, int additionalSmSeats = 0, int additionalServiceAccounts = 0)
int additionalStorageGb, bool premiumAccessAddon, int additionalSmSeats, int additionalServiceAccounts)
{
Items = new List<SubscriptionItemOptions>();
Metadata = new Dictionary<string, string>
@@ -95,9 +95,9 @@ public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionO
{
public OrganizationPurchaseSubscriptionOptions(
Organization org, List<StaticStore.Plan> plans,
TaxInfo taxInfo, int additionalSeats = 0,
int additionalStorageGb = 0, bool premiumAccessAddon = false,
int additionalSmSeats = 0, int additionalServiceAccounts = 0) :
TaxInfo taxInfo, int additionalSeats,
int additionalStorageGb, bool premiumAccessAddon,
int additionalSmSeats, int additionalServiceAccounts) :
base(org, plans, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon, additionalSmSeats, additionalServiceAccounts)
{
OffSession = true;
@@ -109,10 +109,10 @@ public class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOp
{
public OrganizationUpgradeSubscriptionOptions(
string customerId, Organization org,
List<StaticStore.Plan> plans, TaxInfo taxInfo,
int additionalSeats = 0, int additionalStorageGb = 0,
bool premiumAccessAddon = false, int additionalSmSeats = 0, int additionalServiceAccounts = 0) :
base(org, plans, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon, additionalSmSeats, additionalServiceAccounts)
List<StaticStore.Plan> plans, OrganizationUpgrade upgrade) :
base(org, plans, upgrade.TaxInfo, upgrade.AdditionalSeats, upgrade.AdditionalStorageGb,
upgrade.PremiumAccessAddon, upgrade.AdditionalSmSeats.GetValueOrDefault(),
upgrade.AdditionalServiceAccounts.GetValueOrDefault())
{
Customer = customerId;
}

View File

@@ -240,7 +240,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
if (currentSeats > update.SmSeats)
{
throw new BadRequestException($"Your organization currently has {currentSeats} Secrets Manager seats. " +
$"Your plan only allows ({update.SmSeats}) Secrets Manager seats. Remove some Secrets Manager users.");
$"Your plan only allows {update.SmSeats} Secrets Manager seats. Remove some Secrets Manager users.");
}
}
}
@@ -294,7 +294,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
if (currentServiceAccounts > update.SmServiceAccounts)
{
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} Service Accounts. " +
$"Your plan only allows ({update.SmServiceAccounts}) Service Accounts. Remove some Service Accounts.");
$"Your plan only allows {update.SmServiceAccounts} Service Accounts. Remove some Service Accounts.");
}
}
}

View File

@@ -225,9 +225,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
: StaticStore.Plans.Where(p => p.Type == upgrade.Plan && p.BitwardenProduct == BitwardenProductType.PasswordManager).ToList();
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization,
organizationUpgradePlan,
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo
, upgrade.AdditionalSmSeats.GetValueOrDefault(), upgrade.AdditionalServiceAccounts.GetValueOrDefault());
organizationUpgradePlan, upgrade);
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
}
else

View File

@@ -14,9 +14,7 @@ public interface IPaymentService
int additionalServiceAccount = 0);
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
Task<string> UpgradeFreeOrganizationAsync(Organization org, List<Plan> plans,
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo,
int additionalSmSeats = 0, int additionalServiceAccounts = 0);
Task<string> UpgradeFreeOrganizationAsync(Organization org, List<Plan> plans, OrganizationUpgrade upgrade);
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
short additionalStorageGb, TaxInfo taxInfo);
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);

View File

@@ -224,8 +224,7 @@ public class StripePaymentService : IPaymentService
ChangeOrganizationSponsorship(org, sponsorship, false);
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, List<StaticStore.Plan> plans,
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo,
int additionalSmSeats = 0, int additionalServiceAccounts = 0)
OrganizationUpgrade upgrade)
{
if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
{
@@ -241,6 +240,7 @@ public class StripePaymentService : IPaymentService
throw new GatewayException("Could not find customer payment profile.");
}
var taxInfo = upgrade.TaxInfo;
if (taxInfo != null && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressCountry) && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressPostalCode))
{
var taxRateSearch = new TaxRate
@@ -258,7 +258,7 @@ public class StripePaymentService : IPaymentService
}
}
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plans, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon);
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plans, upgrade);
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,

View File

@@ -524,7 +524,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(update));
Assert.Contains("Your organization currently has 8 Secrets Manager seats. Your plan only allows (7) Secrets Manager seats. Remove some Secrets Manager users", exception.Message);
Assert.Contains("Your organization currently has 8 Secrets Manager seats. Your plan only allows 7 Secrets Manager seats. Remove some Secrets Manager users", exception.Message);
await VerifyDependencyNotCalledAsync(sutProvider);
}
@@ -724,7 +724,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
.Returns(currentServiceAccounts);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSecretsManagerSubscription(organizationUpdate));
Assert.Contains("Your organization currently has 301 Service Accounts. Your plan only allows (300) Service Accounts. Remove some Service Accounts", exception.Message);
Assert.Contains("Your organization currently has 301 Service Accounts. Your plan only allows 300 Service Accounts. Remove some Service Accounts", exception.Message);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1).GetServiceAccountCountByOrganizationIdAsync(organization.Id);
await VerifyDependencyNotCalledAsync(sutProvider);
}

View File

@@ -194,20 +194,16 @@ public class OrganizationServiceTests
[Theory]
[BitAutoData]
public async Task SignUpAsync_SecretManagerValidation_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
public async Task SignUpAsync_SecretManager_AdditionalServiceAccounts_NotAllowedByPlan_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
{
signup.AdditionalSmSeats = 10;
signup.AdditionalSeats = 10;
signup.Plan = PlanType.EnterpriseAnnually;
signup.AdditionalSmSeats = 0;
signup.AdditionalSeats = 0;
signup.Plan = PlanType.Free;
signup.UseSecretsManager = true;
signup.PaymentMethodType = PaymentMethodType.Card;
signup.PremiumAccessAddon = false;
signup.AdditionalServiceAccounts = 10;
var purchaseOrganizationPlan = StaticStore.Plans.Where(x => x.Type == signup.Plan && x.BitwardenProduct == BitwardenProductType.PasswordManager).ToList();
var secretsManagerPlan = StaticStore.GetSecretsManagerPlan(PlanType.EnterpriseAnnually);
secretsManagerPlan.HasAdditionalServiceAccountOption = false;
purchaseOrganizationPlan.Add(secretsManagerPlan);
signup.AdditionalStorageGb = 0;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SignUpAsync(signup));

View File

@@ -443,6 +443,8 @@ public class StripePaymentServiceTests
public async void PurchaseOrganizationAsync_SM_Paypal(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
{
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
var passwordManagerPlan = plans.Single(p => p.BitwardenProduct == BitwardenProductType.PasswordManager);
var secretsManagerPlan = plans.Single(p => p.BitwardenProduct == BitwardenProductType.SecretsManager);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
@@ -465,8 +467,12 @@ public class StripePaymentServiceTests
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
var additionalStorage = (short)2;
var additionalSeats = 10;
var additionalSmSeats = 5;
var additionalServiceAccounts = 20;
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plans,
2, 10, false, taxInfo, false, 10, 10);
additionalStorage, additionalSeats, false, taxInfo, false, additionalSmSeats, additionalServiceAccounts);
Assert.Null(result);
Assert.Equal(GatewayType.Stripe, organization.Gateway);
@@ -495,7 +501,11 @@ public class StripePaymentServiceTests
s.Customer == "C-1" &&
s.Expand[0] == "latest_invoice.payment_intent" &&
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
s.Items.Count > 0
s.Items.Count == 4 &&
s.Items.Count(i => i.Plan == passwordManagerPlan.StripeSeatPlanId && i.Quantity == additionalSeats) == 1 &&
s.Items.Count(i => i.Plan == passwordManagerPlan.StripeStoragePlanId && i.Quantity == additionalStorage) == 1 &&
s.Items.Count(i => i.Plan == secretsManagerPlan.StripeSeatPlanId && i.Quantity == additionalSmSeats) == 1 &&
s.Items.Count(i => i.Plan == secretsManagerPlan.StripeServiceAccountPlanId && i.Quantity == additionalServiceAccounts) == 1
));
}
@@ -600,7 +610,17 @@ public class StripePaymentServiceTests
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plans, 0, 0, false, taxInfo);
var upgrade = new OrganizationUpgrade()
{
AdditionalStorageGb = 0,
AdditionalSeats = 0,
PremiumAccessAddon = false,
TaxInfo = taxInfo,
AdditionalSmSeats = 0,
AdditionalServiceAccounts = 0
};
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plans, upgrade);
Assert.Null(result);
}
@@ -626,9 +646,18 @@ public class StripePaymentServiceTests
});
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
var upgrade = new OrganizationUpgrade()
{
AdditionalStorageGb = 1,
AdditionalSeats = 10,
PremiumAccessAddon = false,
TaxInfo = taxInfo,
AdditionalSmSeats = 5,
AdditionalServiceAccounts = 50
};
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plans, 0, 0,
false, taxInfo);
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plans, upgrade);
Assert.Null(result);
}