Skip to content

Commit

Permalink
Merge branch 'main' into billing/AC-2471/unlink-client-without-stripe
Browse files Browse the repository at this point in the history
  • Loading branch information
amorask-bitwarden committed Apr 19, 2024
2 parents fe3ad0b + 821f762 commit 6003d10
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 8 deletions.
34 changes: 30 additions & 4 deletions src/Admin/AdminConsole/Controllers/OrganizationsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
using Bit.Admin.Enums;
using Bit.Admin.Services;
using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Extensions;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
Expand Down Expand Up @@ -53,6 +55,8 @@ public class OrganizationsController : Controller
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
private readonly IFeatureService _featureService;
private readonly IScaleSeatsCommand _scaleSeatsCommand;

public OrganizationsController(
IOrganizationService organizationService,
Expand All @@ -78,7 +82,9 @@ public class OrganizationsController : Controller
IServiceAccountRepository serviceAccountRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IRemovePaymentMethodCommand removePaymentMethodCommand)
IRemovePaymentMethodCommand removePaymentMethodCommand,
IFeatureService featureService,
IScaleSeatsCommand scaleSeatsCommand)
{
_organizationService = organizationService;
_organizationRepository = organizationRepository;
Expand All @@ -104,6 +110,8 @@ public class OrganizationsController : Controller
_providerOrganizationRepository = providerOrganizationRepository;
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_removePaymentMethodCommand = removePaymentMethodCommand;
_featureService = featureService;
_scaleSeatsCommand = scaleSeatsCommand;
}

[RequirePermission(Permission.Org_List_View)]
Expand Down Expand Up @@ -234,12 +242,30 @@ await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventTy
public async Task<IActionResult> Delete(Guid id)
{
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization != null)

if (organization == null)
{
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
return RedirectToAction("Index");
}

var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);

if (consolidatedBillingEnabled && organization.IsValidClient())
{
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);

if (provider.IsBillable())
{
await _scaleSeatsCommand.ScalePasswordManagerSeats(
provider,
organization.PlanType,
-organization.Seats ?? 0);
}
}

await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);

return RedirectToAction("Index");
}

Expand Down
26 changes: 23 additions & 3 deletions src/Api/AdminConsole/Controllers/OrganizationsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Queries;
using Bit.Core.Context;
Expand Down Expand Up @@ -69,6 +70,8 @@ public class OrganizationsController : Controller
private readonly ISubscriberQueries _subscriberQueries;
private readonly IReferenceEventService _referenceEventService;
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
private readonly IProviderRepository _providerRepository;
private readonly IScaleSeatsCommand _scaleSeatsCommand;

public OrganizationsController(
IOrganizationRepository organizationRepository,
Expand All @@ -95,7 +98,9 @@ public class OrganizationsController : Controller
ICancelSubscriptionCommand cancelSubscriptionCommand,
ISubscriberQueries subscriberQueries,
IReferenceEventService referenceEventService,
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand)
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand,
IProviderRepository providerRepository,
IScaleSeatsCommand scaleSeatsCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
Expand All @@ -122,6 +127,8 @@ public class OrganizationsController : Controller
_subscriberQueries = subscriberQueries;
_referenceEventService = referenceEventService;
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
_providerRepository = providerRepository;
_scaleSeatsCommand = scaleSeatsCommand;
}

[HttpGet("{id}")]
Expand Down Expand Up @@ -560,10 +567,23 @@ public async Task Delete(string id, [FromBody] SecretVerificationRequestModel mo
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
else

var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);

if (consolidatedBillingEnabled && organization.IsValidClient())
{
await _organizationService.DeleteAsync(organization);
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);

if (provider.IsBillable())
{
await _scaleSeatsCommand.ScalePasswordManagerSeats(
provider,
organization.PlanType,
-organization.Seats ?? 0);
}
}

await _organizationService.DeleteAsync(organization);
}

[HttpPost("{id}/import")]
Expand Down
17 changes: 17 additions & 0 deletions src/Core/Billing/Extensions/BillingExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Enums;

namespace Bit.Core.Billing.Extensions;

public static class BillingExtensions
{
public static bool IsBillable(this Provider provider) =>
provider is
{
Type: ProviderType.Msp,
Status: ProviderStatusType.Billable
};

public static bool IsValidClient(this Organization organization)
=> organization is
{
Seats: not null,
Status: OrganizationStatusType.Managed,
PlanType: PlanType.TeamsMonthly or PlanType.EnterpriseMonthly
};

public static bool IsStripeEnabled(this Organization organization)
=> !string.IsNullOrEmpty(organization.GatewayCustomerId) &&
!string.IsNullOrEmpty(organization.GatewaySubscriptionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
using AutoFixture.Xunit2;
using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Models.Request.Organizations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
using Bit.Core.AdminConsole.Repositories;
Expand All @@ -25,6 +28,7 @@
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Services;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
Expand Down Expand Up @@ -59,6 +63,8 @@ public class OrganizationsControllerTests : IDisposable
private readonly ISubscriberQueries _subscriberQueries;
private readonly IReferenceEventService _referenceEventService;
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
private readonly IProviderRepository _providerRepository;
private readonly IScaleSeatsCommand _scaleSeatsCommand;

private readonly OrganizationsController _sut;

Expand Down Expand Up @@ -89,6 +95,8 @@ public OrganizationsControllerTests()
_subscriberQueries = Substitute.For<ISubscriberQueries>();
_referenceEventService = Substitute.For<IReferenceEventService>();
_organizationEnableCollectionEnhancementsCommand = Substitute.For<IOrganizationEnableCollectionEnhancementsCommand>();
_providerRepository = Substitute.For<IProviderRepository>();
_scaleSeatsCommand = Substitute.For<IScaleSeatsCommand>();

_sut = new OrganizationsController(
_organizationRepository,
Expand All @@ -115,7 +123,9 @@ public OrganizationsControllerTests()
_cancelSubscriptionCommand,
_subscriberQueries,
_referenceEventService,
_organizationEnableCollectionEnhancementsCommand);
_organizationEnableCollectionEnhancementsCommand,
_providerRepository,
_scaleSeatsCommand);
}

public void Dispose()
Expand Down Expand Up @@ -414,4 +424,39 @@ public async Task EnableCollectionEnhancements_WhenNotOwner_Throws(Organization
await _organizationEnableCollectionEnhancementsCommand.DidNotReceiveWithAnyArgs().EnableCollectionEnhancements(Arg.Any<Organization>());
await _pushNotificationService.DidNotReceiveWithAnyArgs().PushSyncOrganizationsAsync(Arg.Any<Guid>());
}

[Theory, AutoData]
public async Task Delete_OrganizationIsConsolidatedBillingClient_ScalesProvidersSeats(
Provider provider,
Organization organization,
User user,
Guid organizationId,
SecretVerificationRequestModel requestModel)
{
organization.Status = OrganizationStatusType.Managed;
organization.PlanType = PlanType.TeamsMonthly;
organization.Seats = 10;

provider.Type = ProviderType.Msp;
provider.Status = ProviderStatusType.Billable;

_currentContext.OrganizationOwner(organizationId).Returns(true);

_organizationRepository.GetByIdAsync(organizationId).Returns(organization);

_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);

_userService.VerifySecretAsync(user, requestModel.Secret).Returns(true);

_featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);

_providerRepository.GetByOrganizationIdAsync(organization.Id).Returns(provider);

await _sut.Delete(organizationId.ToString(), requestModel);

await _scaleSeatsCommand.Received(1)
.ScalePasswordManagerSeats(provider, organization.PlanType, -organization.Seats.Value);

await _organizationService.Received(1).DeleteAsync(organization);
}
}

0 comments on commit 6003d10

Please sign in to comment.