mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[PM-13450] Admin: Display Multi-organization Enterprise attributes on provider details (#4955)
This commit is contained in:
parent
fc719efee9
commit
35b0f61986
@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -437,144 +438,142 @@ public class ProviderBillingService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateSeatMinimums(
|
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||||
Provider provider,
|
|
||||||
int enterpriseSeatMinimum,
|
|
||||||
int teamsSeatMinimum)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(provider);
|
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
||||||
|
|
||||||
if (enterpriseSeatMinimum < 0 || teamsSeatMinimum < 0)
|
if (plan == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Provider plan not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.PlanType == command.NewPlan)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType);
|
||||||
|
|
||||||
|
plan.PlanType = command.NewPlan;
|
||||||
|
await providerPlanRepository.ReplaceAsync(plan);
|
||||||
|
|
||||||
|
Subscription subscription;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
throw new ConflictException("Subscription not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldSubscriptionItem = subscription.Items.SingleOrDefault(x =>
|
||||||
|
x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId);
|
||||||
|
|
||||||
|
var updateOptions = new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
Items =
|
||||||
|
[
|
||||||
|
new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId,
|
||||||
|
Quantity = oldSubscriptionItem!.Quantity
|
||||||
|
},
|
||||||
|
new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = oldSubscriptionItem.Id,
|
||||||
|
Deleted = true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||||
|
{
|
||||||
|
if (command.Configuration.Any(x => x.SeatsMinimum < 0))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Provider seat minimums must be at least 0.");
|
throw new BadRequestException("Provider seat minimums must be at least 0.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId);
|
Subscription subscription;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, command.Id);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
throw new ConflictException("Subscription not found.");
|
||||||
|
}
|
||||||
|
|
||||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
||||||
|
|
||||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
var providerPlans = await providerPlanRepository.GetByProviderId(command.Id);
|
||||||
|
|
||||||
var enterpriseProviderPlan =
|
foreach (var newPlanConfiguration in command.Configuration)
|
||||||
providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
|
|
||||||
|
|
||||||
if (enterpriseProviderPlan.SeatMinimum != enterpriseSeatMinimum)
|
|
||||||
{
|
{
|
||||||
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager
|
var providerPlan =
|
||||||
.StripeProviderPortalSeatPlanId;
|
providerPlans.Single(providerPlan => providerPlan.PlanType == newPlanConfiguration.Plan);
|
||||||
|
|
||||||
var enterpriseSubscriptionItem = subscription.Items.First(item => item.Price.Id == enterprisePriceId);
|
if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum)
|
||||||
|
|
||||||
if (enterpriseProviderPlan.PurchasedSeats == 0)
|
|
||||||
{
|
{
|
||||||
if (enterpriseProviderPlan.AllocatedSeats > enterpriseSeatMinimum)
|
var priceId = StaticStore.GetPlan(newPlanConfiguration.Plan).PasswordManager
|
||||||
{
|
.StripeProviderPortalSeatPlanId;
|
||||||
enterpriseProviderPlan.PurchasedSeats =
|
var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId);
|
||||||
enterpriseProviderPlan.AllocatedSeats - enterpriseSeatMinimum;
|
|
||||||
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
if (providerPlan.PurchasedSeats == 0)
|
||||||
|
{
|
||||||
|
if (providerPlan.AllocatedSeats > newPlanConfiguration.SeatsMinimum)
|
||||||
{
|
{
|
||||||
Id = enterpriseSubscriptionItem.Id,
|
providerPlan.PurchasedSeats = providerPlan.AllocatedSeats - newPlanConfiguration.SeatsMinimum;
|
||||||
Price = enterprisePriceId,
|
|
||||||
Quantity = enterpriseProviderPlan.AllocatedSeats
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
});
|
{
|
||||||
|
Id = subscriptionItem.Id,
|
||||||
|
Price = priceId,
|
||||||
|
Quantity = providerPlan.AllocatedSeats
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = subscriptionItem.Id,
|
||||||
|
Price = priceId,
|
||||||
|
Quantity = newPlanConfiguration.SeatsMinimum
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
var totalSeats = providerPlan.SeatMinimum + providerPlan.PurchasedSeats;
|
||||||
|
|
||||||
|
if (newPlanConfiguration.SeatsMinimum <= totalSeats)
|
||||||
{
|
{
|
||||||
Id = enterpriseSubscriptionItem.Id,
|
providerPlan.PurchasedSeats = totalSeats - newPlanConfiguration.SeatsMinimum;
|
||||||
Price = enterprisePriceId,
|
}
|
||||||
Quantity = enterpriseSeatMinimum
|
else
|
||||||
});
|
{
|
||||||
|
providerPlan.PurchasedSeats = 0;
|
||||||
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = subscriptionItem.Id,
|
||||||
|
Price = priceId,
|
||||||
|
Quantity = newPlanConfiguration.SeatsMinimum
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
providerPlan.SeatMinimum = newPlanConfiguration.SeatsMinimum;
|
||||||
|
|
||||||
|
await providerPlanRepository.ReplaceAsync(providerPlan);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var totalEnterpriseSeats = enterpriseProviderPlan.SeatMinimum + enterpriseProviderPlan.PurchasedSeats;
|
|
||||||
|
|
||||||
if (enterpriseSeatMinimum <= totalEnterpriseSeats)
|
|
||||||
{
|
|
||||||
enterpriseProviderPlan.PurchasedSeats = totalEnterpriseSeats - enterpriseSeatMinimum;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
enterpriseProviderPlan.PurchasedSeats = 0;
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = enterpriseSubscriptionItem.Id,
|
|
||||||
Price = enterprisePriceId,
|
|
||||||
Quantity = enterpriseSeatMinimum
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enterpriseProviderPlan.SeatMinimum = enterpriseSeatMinimum;
|
|
||||||
|
|
||||||
await providerPlanRepository.ReplaceAsync(enterpriseProviderPlan);
|
|
||||||
}
|
|
||||||
|
|
||||||
var teamsProviderPlan =
|
|
||||||
providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
|
|
||||||
|
|
||||||
if (teamsProviderPlan.SeatMinimum != teamsSeatMinimum)
|
|
||||||
{
|
|
||||||
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager
|
|
||||||
.StripeProviderPortalSeatPlanId;
|
|
||||||
|
|
||||||
var teamsSubscriptionItem = subscription.Items.First(item => item.Price.Id == teamsPriceId);
|
|
||||||
|
|
||||||
if (teamsProviderPlan.PurchasedSeats == 0)
|
|
||||||
{
|
|
||||||
if (teamsProviderPlan.AllocatedSeats > teamsSeatMinimum)
|
|
||||||
{
|
|
||||||
teamsProviderPlan.PurchasedSeats = teamsProviderPlan.AllocatedSeats - teamsSeatMinimum;
|
|
||||||
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = teamsSubscriptionItem.Id,
|
|
||||||
Price = teamsPriceId,
|
|
||||||
Quantity = teamsProviderPlan.AllocatedSeats
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = teamsSubscriptionItem.Id,
|
|
||||||
Price = teamsPriceId,
|
|
||||||
Quantity = teamsSeatMinimum
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var totalTeamsSeats = teamsProviderPlan.SeatMinimum + teamsProviderPlan.PurchasedSeats;
|
|
||||||
|
|
||||||
if (teamsSeatMinimum <= totalTeamsSeats)
|
|
||||||
{
|
|
||||||
teamsProviderPlan.PurchasedSeats = totalTeamsSeats - teamsSeatMinimum;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
teamsProviderPlan.PurchasedSeats = 0;
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = teamsSubscriptionItem.Id,
|
|
||||||
Price = teamsPriceId,
|
|
||||||
Quantity = teamsSeatMinimum
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
teamsProviderPlan.SeatMinimum = teamsSeatMinimum;
|
|
||||||
|
|
||||||
await providerPlanRepository.ReplaceAsync(teamsProviderPlan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscriptionItemOptionsList.Count > 0)
|
if (subscriptionItemOptionsList.Count > 0)
|
||||||
{
|
{
|
||||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId,
|
||||||
new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });
|
new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ using Bit.Core.Billing.Entities;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -1011,26 +1012,192 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region UpdateSeatMinimums
|
#region ChangePlan
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateSeatMinimums_NullProvider_ThrowsArgumentNullException(
|
public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException(
|
||||||
SutProvider<ProviderBillingService> sutProvider) =>
|
ChangeProviderPlanCommand command,
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdateSeatMinimums(null, 0, 0));
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
providerPlanRepository.GetByIdAsync(Arg.Any<Guid>()).Returns((ProviderPlan)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.ChangePlan(command));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Provider plan not found.", actual.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ChangePlan_ProviderNotFound_DoesNothing(
|
||||||
|
ChangeProviderPlanCommand command,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
var existingPlan = new ProviderPlan
|
||||||
|
{
|
||||||
|
Id = command.ProviderPlanId,
|
||||||
|
PlanType = command.NewPlan,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0,
|
||||||
|
SeatMinimum = 0
|
||||||
|
};
|
||||||
|
providerPlanRepository
|
||||||
|
.GetByIdAsync(Arg.Is<Guid>(p => p == command.ProviderPlanId))
|
||||||
|
.Returns(existingPlan);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.ChangePlan(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());
|
||||||
|
await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ChangePlan_SameProviderPlan_DoesNothing(
|
||||||
|
ChangeProviderPlanCommand command,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
var existingPlan = new ProviderPlan
|
||||||
|
{
|
||||||
|
Id = command.ProviderPlanId,
|
||||||
|
PlanType = command.NewPlan,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0,
|
||||||
|
SeatMinimum = 0
|
||||||
|
};
|
||||||
|
providerPlanRepository
|
||||||
|
.GetByIdAsync(Arg.Is<Guid>(p => p == command.ProviderPlanId))
|
||||||
|
.Returns(existingPlan);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.ChangePlan(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());
|
||||||
|
await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ChangePlan_UpdatesSubscriptionCorrectly(
|
||||||
|
Guid providerPlanId,
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var existingPlan = new ProviderPlan
|
||||||
|
{
|
||||||
|
Id = providerPlanId,
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
PlanType = PlanType.EnterpriseAnnually,
|
||||||
|
PurchasedSeats = 2,
|
||||||
|
AllocatedSeats = 10,
|
||||||
|
SeatMinimum = 8
|
||||||
|
};
|
||||||
|
providerPlanRepository
|
||||||
|
.GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId))
|
||||||
|
.Returns(existingPlan);
|
||||||
|
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(Arg.Is(existingPlan.ProviderId)).Returns(provider);
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.ProviderSubscriptionGetAsync(
|
||||||
|
Arg.Is(provider.GatewaySubscriptionId),
|
||||||
|
Arg.Is(provider.Id))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Id = provider.GatewaySubscriptionId,
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data =
|
||||||
|
[
|
||||||
|
new SubscriptionItem
|
||||||
|
{
|
||||||
|
Id = "si_ent_annual",
|
||||||
|
Price = new Price
|
||||||
|
{
|
||||||
|
Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager
|
||||||
|
.StripeProviderPortalSeatPlanId
|
||||||
|
},
|
||||||
|
Quantity = 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var command =
|
||||||
|
new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.ChangePlan(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerPlanRepository.Received(1)
|
||||||
|
.ReplaceAsync(Arg.Is<ProviderPlan>(p => p.PlanType == PlanType.EnterpriseMonthly));
|
||||||
|
|
||||||
|
await stripeAdapter.Received(1)
|
||||||
|
.SubscriptionUpdateAsync(
|
||||||
|
Arg.Is(provider.GatewaySubscriptionId),
|
||||||
|
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||||
|
p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1));
|
||||||
|
|
||||||
|
var newPlanCfg = StaticStore.GetPlan(command.NewPlan);
|
||||||
|
await stripeAdapter.Received(1)
|
||||||
|
.SubscriptionUpdateAsync(
|
||||||
|
Arg.Is(provider.GatewaySubscriptionId),
|
||||||
|
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||||
|
p.Items.Count(si =>
|
||||||
|
si.Price == newPlanCfg.PasswordManager.StripeProviderPortalSeatPlanId &&
|
||||||
|
si.Deleted == default &&
|
||||||
|
si.Quantity == 10) == 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region UpdateSeatMinimums
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestException(
|
public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestException(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider) =>
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSeatMinimums(provider, -10, 100));
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.TeamsMonthly, -10),
|
||||||
|
(PlanType.EnterpriseMonthly, 50)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSeatMinimums(command));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Provider seat minimums must be at least 0.", actual.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomingMinimum_UpdatesPurchasedSeats_SyncsStripeWithNewSeatMinimum(
|
public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomingMinimum_UpdatesPurchasedSeats_SyncsStripeWithNewSeatMinimum(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1058,7 +1225,9 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1066,10 +1235,21 @@ public class ProviderBillingServiceTests
|
|||||||
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
|
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 30, 20);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 30),
|
||||||
|
(PlanType.TeamsMonthly, 20)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 30));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 30));
|
||||||
|
|
||||||
@ -1091,8 +1271,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1120,7 +1303,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1130,8 +1313,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 70, 50);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 70),
|
||||||
|
(PlanType.TeamsMonthly, 50)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
||||||
|
|
||||||
@ -1153,8 +1346,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1182,7 +1378,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1192,8 +1388,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 60, 60);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 60),
|
||||||
|
(PlanType.TeamsMonthly, 60)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));
|
||||||
|
|
||||||
@ -1209,8 +1415,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1238,7 +1447,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1248,8 +1457,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 80, 80);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 80),
|
||||||
|
(PlanType.TeamsMonthly, 80)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));
|
||||||
|
|
||||||
@ -1271,8 +1490,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1300,7 +1522,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1310,8 +1532,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 70, 30);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 70),
|
||||||
|
(PlanType.TeamsMonthly, 30)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -290,25 +291,39 @@ public class ProvidersController : Controller
|
|||||||
|
|
||||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||||
|
|
||||||
if (providerPlans.Count == 0)
|
switch (provider.Type)
|
||||||
{
|
{
|
||||||
var newProviderPlans = new List<ProviderPlan>
|
case ProviderType.Msp:
|
||||||
{
|
var updateMspSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||||
new () { ProviderId = provider.Id, PlanType = PlanType.TeamsMonthly, SeatMinimum = model.TeamsMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 },
|
provider.Id,
|
||||||
new () { ProviderId = provider.Id, PlanType = PlanType.EnterpriseMonthly, SeatMinimum = model.EnterpriseMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 }
|
provider.GatewaySubscriptionId,
|
||||||
};
|
[
|
||||||
|
(Plan: PlanType.TeamsMonthly, SeatsMinimum: model.TeamsMonthlySeatMinimum),
|
||||||
|
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: model.EnterpriseMonthlySeatMinimum)
|
||||||
|
]);
|
||||||
|
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
||||||
|
break;
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
|
{
|
||||||
|
var existingMoePlan = providerPlans.Single();
|
||||||
|
|
||||||
foreach (var newProviderPlan in newProviderPlans)
|
// 1. Change the plan and take over any old values.
|
||||||
{
|
var changeMoePlanCommand = new ChangeProviderPlanCommand(
|
||||||
await _providerPlanRepository.CreateAsync(newProviderPlan);
|
existingMoePlan.Id,
|
||||||
}
|
model.Plan!.Value,
|
||||||
}
|
provider.GatewaySubscriptionId);
|
||||||
else
|
await _providerBillingService.ChangePlan(changeMoePlanCommand);
|
||||||
{
|
|
||||||
await _providerBillingService.UpdateSeatMinimums(
|
// 2. Update the seat minimums.
|
||||||
provider,
|
var updateMoeSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||||
model.EnterpriseMonthlySeatMinimum,
|
provider.Id,
|
||||||
model.TeamsMonthlySeatMinimum);
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(Plan: model.Plan!.Value, SeatsMinimum: model.EnterpriseMinimumSeats!.Value)
|
||||||
|
]);
|
||||||
|
await _providerBillingService.UpdateSeatMinimums(updateMoeSeatMinimumsCommand);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
|
@ -33,6 +33,12 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
GatewayCustomerUrl = gatewayCustomerUrl;
|
GatewayCustomerUrl = gatewayCustomerUrl;
|
||||||
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
||||||
Type = provider.Type;
|
Type = provider.Type;
|
||||||
|
if (Type == ProviderType.MultiOrganizationEnterprise)
|
||||||
|
{
|
||||||
|
var plan = providerPlans.Single();
|
||||||
|
EnterpriseMinimumSeats = plan.SeatMinimum;
|
||||||
|
Plan = plan.PlanType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Display(Name = "Billing Email")]
|
[Display(Name = "Billing Email")]
|
||||||
@ -58,13 +64,24 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
[Display(Name = "Provider Type")]
|
[Display(Name = "Provider Type")]
|
||||||
public ProviderType Type { get; set; }
|
public ProviderType Type { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Plan")]
|
||||||
|
public PlanType? Plan { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Enterprise Seats Minimum")]
|
||||||
|
public int? EnterpriseMinimumSeats { get; set; }
|
||||||
|
|
||||||
public virtual Provider ToProvider(Provider existingProvider)
|
public virtual Provider ToProvider(Provider existingProvider)
|
||||||
{
|
{
|
||||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
||||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
||||||
existingProvider.Gateway = Gateway;
|
switch (Type)
|
||||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
{
|
||||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
case ProviderType.Msp:
|
||||||
|
existingProvider.Gateway = Gateway;
|
||||||
|
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||||
|
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
return existingProvider;
|
return existingProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +99,23 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
|
if (Plan == null)
|
||||||
|
{
|
||||||
|
var displayName = nameof(Plan).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Plan);
|
||||||
|
yield return new ValidationResult($"The {displayName} field is required.");
|
||||||
|
}
|
||||||
|
if (EnterpriseMinimumSeats == null)
|
||||||
|
{
|
||||||
|
var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
||||||
|
yield return new ValidationResult($"The {displayName} field is required.");
|
||||||
|
}
|
||||||
|
if (EnterpriseMinimumSeats < 0)
|
||||||
|
{
|
||||||
|
var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
||||||
|
yield return new ValidationResult($"The {displayName} field cannot be less than 0.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
@using Bit.Admin.Enums;
|
@using Bit.Admin.Enums;
|
||||||
@using Bit.Core
|
@using Bit.Core
|
||||||
|
@using Bit.Core.AdminConsole.Enums.Provider
|
||||||
|
@using Bit.Core.Billing.Enums
|
||||||
@using Bit.Core.Billing.Extensions
|
@using Bit.Core.Billing.Extensions
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||||
|
|
||||||
@ -47,60 +50,97 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
||||||
{
|
{
|
||||||
<div class="row">
|
switch (Model.Provider.Type)
|
||||||
<div class="col-sm">
|
{
|
||||||
<div class="form-group">
|
case ProviderType.Msp:
|
||||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
{
|
||||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
<div class="row">
|
||||||
</div>
|
<div class="col-sm">
|
||||||
</div>
|
<div class="form-group">
|
||||||
<div class="col-sm">
|
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||||
<div class="form-group">
|
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
</div>
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="Gateway"></label>
|
|
||||||
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
|
||||||
<option value="">--</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-sm">
|
||||||
</div>
|
<div class="form-group">
|
||||||
</div>
|
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||||
<div class="row">
|
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||||
<div class="col-sm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="GatewayCustomerId"></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" asp-for="GatewayCustomerId">
|
|
||||||
<div class="input-group-append">
|
|
||||||
<a href="@Model.GatewayCustomerUrl" class="btn btn-secondary" target="_blank">
|
|
||||||
<i class="fa fa-external-link"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="GatewaySubscriptionId"></label>
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<label asp-for="Gateway"></label>
|
||||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
|
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||||
<div class="input-group-append">
|
<option value="">--</option>
|
||||||
<a href="@Model.GatewaySubscriptionUrl" class="btn btn-secondary" target="_blank">
|
</select>
|
||||||
<i class="fa fa-external-link"></i>
|
</div>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="row">
|
||||||
</div>
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="GatewayCustomerId"></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" asp-for="GatewayCustomerId">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<a href="@Model.GatewayCustomerUrl" class="btn btn-secondary" target="_blank">
|
||||||
|
<i class="fa fa-external-link"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="GatewaySubscriptionId"></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<a href="@Model.GatewaySubscriptionUrl" class="btn btn-secondary" target="_blank">
|
||||||
|
<i class="fa fa-external-link"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
|
{
|
||||||
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) && Model.Provider.Type == ProviderType.MultiOrganizationEnterprise)
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
@{
|
||||||
|
var multiOrgPlans = new List<PlanType>
|
||||||
|
{
|
||||||
|
PlanType.EnterpriseAnnually,
|
||||||
|
PlanType.EnterpriseMonthly
|
||||||
|
};
|
||||||
|
}
|
||||||
|
<label asp-for="Plan"></label>
|
||||||
|
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||||
|
<option value="">--</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EnterpriseMinimumSeats"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
@await Html.PartialAsync("Organizations", Model)
|
@await Html.PartialAsync("Organizations", Model)
|
||||||
|
@ -13,7 +13,7 @@ public static class BillingExtensions
|
|||||||
public static bool IsBillable(this Provider provider) =>
|
public static bool IsBillable(this Provider provider) =>
|
||||||
provider is
|
provider is
|
||||||
{
|
{
|
||||||
Type: ProviderType.Msp,
|
Type: ProviderType.Msp or ProviderType.MultiOrganizationEnterprise,
|
||||||
Status: ProviderStatusType.Billable
|
Status: ProviderStatusType.Billable
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Migration.Models;
|
using Bit.Core.Billing.Migration.Models;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -307,7 +308,14 @@ public class ProviderMigrator(
|
|||||||
.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)?
|
.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)?
|
||||||
.SeatMinimum ?? 0;
|
.SeatMinimum ?? 0;
|
||||||
|
|
||||||
await providerBillingService.UpdateSeatMinimums(provider, enterpriseSeatMinimum, teamsSeatMinimum);
|
var updateSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: enterpriseSeatMinimum),
|
||||||
|
(Plan: PlanType.TeamsMonthly, SeatsMinimum: teamsSeatMinimum)
|
||||||
|
]);
|
||||||
|
await providerBillingService.UpdateSeatMinimums(updateSeatMinimumsCommand);
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"CB: Updated Stripe subscription for provider ({ProviderID}) with current seat minimums", provider.Id);
|
"CB: Updated Stripe subscription for provider ({ProviderID}) with current seat minimums", provider.Id);
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Services.Contracts;
|
||||||
|
|
||||||
|
public record ChangeProviderPlanCommand(
|
||||||
|
Guid ProviderPlanId,
|
||||||
|
PlanType NewPlan,
|
||||||
|
string GatewaySubscriptionId);
|
@ -0,0 +1,10 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Services.Contracts;
|
||||||
|
|
||||||
|
/// <param name="Id">The ID of the provider to update the seat minimums for.</param>
|
||||||
|
/// <param name="Configuration">The new seat minimums for the provider.</param>
|
||||||
|
public record UpdateProviderSeatMinimumsCommand(
|
||||||
|
Guid Id,
|
||||||
|
string GatewaySubscriptionId,
|
||||||
|
IReadOnlyCollection<(PlanType Plan, int SeatsMinimum)> Configuration);
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
@ -89,8 +90,12 @@ public interface IProviderBillingService
|
|||||||
Task<Subscription> SetupSubscription(
|
Task<Subscription> SetupSubscription(
|
||||||
Provider provider);
|
Provider provider);
|
||||||
|
|
||||||
Task UpdateSeatMinimums(
|
/// <summary>
|
||||||
Provider provider,
|
/// Changes the assigned provider plan for the provider.
|
||||||
int enterpriseSeatMinimum,
|
/// </summary>
|
||||||
int teamsSeatMinimum);
|
/// <param name="command">The command to change the provider plan.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task ChangePlan(ChangeProviderPlanCommand command);
|
||||||
|
|
||||||
|
Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,17 @@ public interface IStripeAdapter
|
|||||||
CustomerBalanceTransactionCreateOptions options);
|
CustomerBalanceTransactionCreateOptions options);
|
||||||
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
||||||
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a subscription object for a provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The subscription ID.</param>
|
||||||
|
/// <param name="providerId">The provider ID.</param>
|
||||||
|
/// <param name="options">Additional options.</param>
|
||||||
|
/// <returns>The subscription object.</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown when the subscription doesn't belong to the provider.</exception>
|
||||||
|
Task<Stripe.Subscription> ProviderSubscriptionGetAsync(string id, Guid providerId, Stripe.SubscriptionGetOptions options = null);
|
||||||
|
|
||||||
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
||||||
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
||||||
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
||||||
|
@ -79,6 +79,20 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
return _subscriptionService.GetAsync(id, options);
|
return _subscriptionService.GetAsync(id, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Subscription> ProviderSubscriptionGetAsync(
|
||||||
|
string id,
|
||||||
|
Guid providerId,
|
||||||
|
SubscriptionGetOptions options = null)
|
||||||
|
{
|
||||||
|
var subscription = await _subscriptionService.GetAsync(id, options);
|
||||||
|
if (subscription.Metadata.TryGetValue("providerId", out var value) && value == providerId.ToString())
|
||||||
|
{
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Subscription does not belong to the provider.");
|
||||||
|
}
|
||||||
|
|
||||||
public Task<Stripe.Subscription> SubscriptionUpdateAsync(string id,
|
public Task<Stripe.Subscription> SubscriptionUpdateAsync(string id,
|
||||||
Stripe.SubscriptionUpdateOptions options = null)
|
Stripe.SubscriptionUpdateOptions options = null)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user