mirror of
https://github.com/bitwarden/server.git
synced 2025-04-06 13:38:13 -05:00
[AC-2312] Server: Update ProviderOrganizationsController.Delete to update provider plan (#4008)
* initial commit Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * fix the failing unit test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Resolve some pr comments Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * resolves some pr comments Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * resolve the collection expression suggestion Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * resolve pr comments Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * test for when the flag is on Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * rename the test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> --------- Signed-off-by: Cy Okeke <cokeke@bitwarden.com>
This commit is contained in:
parent
eac2b9f0b8
commit
b220de0126
@ -1,11 +1,16 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Commands;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
@ -20,6 +25,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||||
private readonly IStripeAdapter _stripeAdapter;
|
private readonly IStripeAdapter _stripeAdapter;
|
||||||
|
private readonly IScaleSeatsCommand _scaleSeatsCommand;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public RemoveOrganizationFromProviderCommand(
|
public RemoveOrganizationFromProviderCommand(
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
@ -28,7 +35,9 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IStripeAdapter stripeAdapter)
|
IStripeAdapter stripeAdapter,
|
||||||
|
IScaleSeatsCommand scaleSeatsCommand,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -37,6 +46,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_providerOrganizationRepository = providerOrganizationRepository;
|
_providerOrganizationRepository = providerOrganizationRepository;
|
||||||
_stripeAdapter = stripeAdapter;
|
_stripeAdapter = stripeAdapter;
|
||||||
|
_scaleSeatsCommand = scaleSeatsCommand;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveOrganizationFromProvider(
|
public async Task RemoveOrganizationFromProvider(
|
||||||
@ -65,8 +76,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
|
|
||||||
organization.BillingEmail = organizationOwnerEmails.MinBy(email => email);
|
organization.BillingEmail = organizationOwnerEmails.MinBy(email => email);
|
||||||
|
|
||||||
await _organizationRepository.ReplaceAsync(organization);
|
|
||||||
|
|
||||||
var customerUpdateOptions = new CustomerUpdateOptions
|
var customerUpdateOptions = new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Coupon = string.Empty,
|
Coupon = string.Empty,
|
||||||
@ -75,13 +84,41 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
|
|
||||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, customerUpdateOptions);
|
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, customerUpdateOptions);
|
||||||
|
|
||||||
|
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||||
|
|
||||||
|
if (isConsolidatedBillingEnabled && provider.Status == ProviderStatusType.Billable)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager;
|
||||||
|
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||||
|
{
|
||||||
|
Customer = organization.GatewayCustomerId,
|
||||||
|
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
|
||||||
|
DaysUntilDue = 30,
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "organizationId", organization.Id.ToString() }
|
||||||
|
},
|
||||||
|
OffSession = true,
|
||||||
|
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
||||||
|
Items = [new SubscriptionItemOptions { Price = plan.StripeSeatPlanId, Quantity = organization.Seats }]
|
||||||
|
};
|
||||||
|
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
|
organization.GatewaySubscriptionId = subscription.Id;
|
||||||
|
await _scaleSeatsCommand.ScalePasswordManagerSeats(provider, organization.PlanType,
|
||||||
|
-(organization.Seats ?? 0));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
CollectionMethod = "send_invoice",
|
CollectionMethod = "send_invoice",
|
||||||
DaysUntilDue = 30
|
DaysUntilDue = 30
|
||||||
};
|
};
|
||||||
|
|
||||||
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
|
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
|
||||||
await _mailService.SendProviderUpdatePaymentMethod(
|
await _mailService.SendProviderUpdatePaymentMethod(
|
||||||
organization.Id,
|
organization.Id,
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Commands;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -81,7 +84,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations(
|
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations__FeatureFlagOff(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
ProviderOrganization providerOrganization,
|
ProviderOrganization providerOrganization,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
@ -97,30 +100,90 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
includeProvider: false)
|
includeProvider: false)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
var organizationOwnerEmails = new List<string> { "a@gmail.com", "b@gmail.com" };
|
var organizationOwnerEmails = new List<string> { "a@example.com", "b@example.com" };
|
||||||
|
|
||||||
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
|
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||||
|
{
|
||||||
|
Id = "S-1",
|
||||||
|
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||||
|
});
|
||||||
|
|
||||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||||
|
|
||||||
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
|
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||||
org => org.Id == organization.Id && org.BillingEmail == "a@gmail.com"));
|
org => org.Id == organization.Id && org.BillingEmail == "a@example.com"));
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
|
|
||||||
await stripeAdapter.Received(1).CustomerUpdateAsync(
|
await stripeAdapter.Received(1).CustomerUpdateAsync(
|
||||||
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
||||||
options => options.Coupon == string.Empty && options.Email == "a@gmail.com"));
|
options => options.Coupon == string.Empty && options.Email == "a@example.com"));
|
||||||
|
|
||||||
await stripeAdapter.Received(1).SubscriptionUpdateAsync(
|
|
||||||
organization.GatewaySubscriptionId, Arg.Is<SubscriptionUpdateOptions>(
|
|
||||||
options => options.CollectionMethod == "send_invoice" && options.DaysUntilDue == 30));
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
|
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
|
||||||
organization.Id,
|
organization.Id,
|
||||||
organization.Name,
|
organization.Name,
|
||||||
provider.Name,
|
provider.Name,
|
||||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains("a@gmail.com") && emails.Contains("b@gmail.com")));
|
Arg.Is<IEnumerable<string>>(emails => emails.Contains("a@example.com") && emails.Contains("b@example.com")));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||||
|
.DeleteAsync(providerOrganization);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventAsync(
|
||||||
|
providerOrganization,
|
||||||
|
EventType.ProviderOrganization_Removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RemoveOrganizationFromProvider_CreatesSubscriptionAndScalesSeats_FeatureFlagON(Provider provider,
|
||||||
|
ProviderOrganization providerOrganization,
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
providerOrganization.ProviderId = provider.Id;
|
||||||
|
provider.Status = ProviderStatusType.Billable;
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
|
||||||
|
providerOrganization.OrganizationId,
|
||||||
|
Array.Empty<Guid>(),
|
||||||
|
includeProvider: false)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var organizationOwnerEmails = new List<string> { "a@example.com", "b@example.com" };
|
||||||
|
|
||||||
|
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||||
|
{
|
||||||
|
Id = "S-1",
|
||||||
|
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||||
|
});
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
|
||||||
|
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||||
|
await stripeAdapter.Received(1).CustomerUpdateAsync(
|
||||||
|
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
||||||
|
options => options.Coupon == string.Empty && options.Email == "a@example.com"));
|
||||||
|
|
||||||
|
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(c =>
|
||||||
|
c.Customer == organization.GatewayCustomerId &&
|
||||||
|
c.CollectionMethod == "send_invoice" &&
|
||||||
|
c.DaysUntilDue == 30 &&
|
||||||
|
c.Items.Count == 1
|
||||||
|
));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IScaleSeatsCommand>().Received(1)
|
||||||
|
.ScalePasswordManagerSeats(provider, organization.PlanType, -(int)organization.Seats);
|
||||||
|
|
||||||
|
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||||
|
org => org.Id == organization.Id && org.BillingEmail == "a@example.com" &&
|
||||||
|
org.GatewaySubscriptionId == "S-1"));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
|
||||||
|
organization.Id,
|
||||||
|
organization.Name,
|
||||||
|
provider.Name,
|
||||||
|
Arg.Is<IEnumerable<string>>(emails =>
|
||||||
|
emails.Contains("a@example.com") && emails.Contains("b@example.com")));
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||||
.DeleteAsync(providerOrganization);
|
.DeleteAsync(providerOrganization);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user