mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
Merge branch 'main' into renovate/stripe.net-46.x
This commit is contained in:
commit
9a3c69642f
@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -437,144 +438,142 @@ public class ProviderBillingService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateSeatMinimums(
|
||||
Provider provider,
|
||||
int enterpriseSeatMinimum,
|
||||
int teamsSeatMinimum)
|
||||
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
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 providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||
var providerPlans = await providerPlanRepository.GetByProviderId(command.Id);
|
||||
|
||||
var enterpriseProviderPlan =
|
||||
providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
|
||||
|
||||
if (enterpriseProviderPlan.SeatMinimum != enterpriseSeatMinimum)
|
||||
foreach (var newPlanConfiguration in command.Configuration)
|
||||
{
|
||||
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager
|
||||
.StripeProviderPortalSeatPlanId;
|
||||
var providerPlan =
|
||||
providerPlans.Single(providerPlan => providerPlan.PlanType == newPlanConfiguration.Plan);
|
||||
|
||||
var enterpriseSubscriptionItem = subscription.Items.First(item => item.Price.Id == enterprisePriceId);
|
||||
|
||||
if (enterpriseProviderPlan.PurchasedSeats == 0)
|
||||
if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum)
|
||||
{
|
||||
if (enterpriseProviderPlan.AllocatedSeats > enterpriseSeatMinimum)
|
||||
{
|
||||
enterpriseProviderPlan.PurchasedSeats =
|
||||
enterpriseProviderPlan.AllocatedSeats - enterpriseSeatMinimum;
|
||||
var priceId = StaticStore.GetPlan(newPlanConfiguration.Plan).PasswordManager
|
||||
.StripeProviderPortalSeatPlanId;
|
||||
var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId);
|
||||
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
if (providerPlan.PurchasedSeats == 0)
|
||||
{
|
||||
if (providerPlan.AllocatedSeats > newPlanConfiguration.SeatsMinimum)
|
||||
{
|
||||
Id = enterpriseSubscriptionItem.Id,
|
||||
Price = enterprisePriceId,
|
||||
Quantity = enterpriseProviderPlan.AllocatedSeats
|
||||
});
|
||||
providerPlan.PurchasedSeats = providerPlan.AllocatedSeats - newPlanConfiguration.SeatsMinimum;
|
||||
|
||||
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
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
var totalSeats = providerPlan.SeatMinimum + providerPlan.PurchasedSeats;
|
||||
|
||||
if (newPlanConfiguration.SeatsMinimum <= totalSeats)
|
||||
{
|
||||
Id = enterpriseSubscriptionItem.Id,
|
||||
Price = enterprisePriceId,
|
||||
Quantity = enterpriseSeatMinimum
|
||||
});
|
||||
providerPlan.PurchasedSeats = totalSeats - newPlanConfiguration.SeatsMinimum;
|
||||
}
|
||||
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)
|
||||
{
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -1011,26 +1012,192 @@ public class ProviderBillingServiceTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateSeatMinimums
|
||||
#region ChangePlan
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateSeatMinimums_NullProvider_ThrowsArgumentNullException(
|
||||
SutProvider<ProviderBillingService> sutProvider) =>
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdateSeatMinimums(null, 0, 0));
|
||||
public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException(
|
||||
ChangeProviderPlanCommand command,
|
||||
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]
|
||||
public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestException(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider) =>
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSeatMinimums(provider, -10, 100));
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// 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]
|
||||
public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomingMinimum_UpdatesPurchasedSeats_SyncsStripeWithNewSeatMinimum(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
|
||||
const string enterpriseLineItemId = "enterprise_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>
|
||||
{
|
||||
@ -1066,10 +1235,21 @@ public class ProviderBillingServiceTests
|
||||
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
|
||||
};
|
||||
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
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>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 30));
|
||||
|
||||
@ -1091,8 +1271,11 @@ public class ProviderBillingServiceTests
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
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 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>
|
||||
{
|
||||
@ -1130,8 +1313,18 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
||||
|
||||
@ -1153,8 +1346,11 @@ public class ProviderBillingServiceTests
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
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 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>
|
||||
{
|
||||
@ -1192,8 +1388,18 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));
|
||||
|
||||
@ -1209,8 +1415,11 @@ public class ProviderBillingServiceTests
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
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 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>
|
||||
{
|
||||
@ -1248,8 +1457,18 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));
|
||||
|
||||
@ -1271,8 +1490,11 @@ public class ProviderBillingServiceTests
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
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 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>
|
||||
{
|
||||
@ -1310,8 +1532,18 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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>(
|
||||
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.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
@ -290,25 +291,39 @@ public class ProvidersController : Controller
|
||||
|
||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||
|
||||
if (providerPlans.Count == 0)
|
||||
switch (provider.Type)
|
||||
{
|
||||
var newProviderPlans = new List<ProviderPlan>
|
||||
{
|
||||
new () { ProviderId = provider.Id, PlanType = PlanType.TeamsMonthly, SeatMinimum = model.TeamsMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 },
|
||||
new () { ProviderId = provider.Id, PlanType = PlanType.EnterpriseMonthly, SeatMinimum = model.EnterpriseMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 }
|
||||
};
|
||||
case ProviderType.Msp:
|
||||
var updateMspSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
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)
|
||||
{
|
||||
await _providerPlanRepository.CreateAsync(newProviderPlan);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _providerBillingService.UpdateSeatMinimums(
|
||||
provider,
|
||||
model.EnterpriseMonthlySeatMinimum,
|
||||
model.TeamsMonthlySeatMinimum);
|
||||
// 1. Change the plan and take over any old values.
|
||||
var changeMoePlanCommand = new ChangeProviderPlanCommand(
|
||||
existingMoePlan.Id,
|
||||
model.Plan!.Value,
|
||||
provider.GatewaySubscriptionId);
|
||||
await _providerBillingService.ChangePlan(changeMoePlanCommand);
|
||||
|
||||
// 2. Update the seat minimums.
|
||||
var updateMoeSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
provider.GatewaySubscriptionId,
|
||||
[
|
||||
(Plan: model.Plan!.Value, SeatsMinimum: model.EnterpriseMinimumSeats!.Value)
|
||||
]);
|
||||
await _providerBillingService.UpdateSeatMinimums(updateMoeSeatMinimumsCommand);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction("Edit", new { id });
|
||||
|
@ -33,6 +33,12 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
GatewayCustomerUrl = gatewayCustomerUrl;
|
||||
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
||||
Type = provider.Type;
|
||||
if (Type == ProviderType.MultiOrganizationEnterprise)
|
||||
{
|
||||
var plan = providerPlans.Single();
|
||||
EnterpriseMinimumSeats = plan.SeatMinimum;
|
||||
Plan = plan.PlanType;
|
||||
}
|
||||
}
|
||||
|
||||
[Display(Name = "Billing Email")]
|
||||
@ -58,13 +64,24 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
[Display(Name = "Provider Type")]
|
||||
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)
|
||||
{
|
||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
||||
existingProvider.Gateway = Gateway;
|
||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
switch (Type)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
existingProvider.Gateway = Gateway;
|
||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
break;
|
||||
}
|
||||
return existingProvider;
|
||||
}
|
||||
|
||||
@ -82,6 +99,23 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||
}
|
||||
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.Core
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Bit.Core.Billing.Enums
|
||||
@using Bit.Core.Billing.Extensions
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
|
||||
@ -47,60 +50,97 @@
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||
<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>
|
||||
switch (Model.Provider.Type)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<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 class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||
</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 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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<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>
|
||||
@await Html.PartialAsync("Organizations", Model)
|
||||
|
@ -13,7 +13,7 @@ public static class BillingExtensions
|
||||
public static bool IsBillable(this Provider provider) =>
|
||||
provider is
|
||||
{
|
||||
Type: ProviderType.Msp,
|
||||
Type: ProviderType.Msp or ProviderType.MultiOrganizationEnterprise,
|
||||
Status: ProviderStatusType.Billable
|
||||
};
|
||||
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Migration.Models;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -307,7 +308,14 @@ public class ProviderMigrator(
|
||||
.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)?
|
||||
.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(
|
||||
"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.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Models.Business;
|
||||
using Stripe;
|
||||
|
||||
@ -89,8 +90,12 @@ public interface IProviderBillingService
|
||||
Task<Subscription> SetupSubscription(
|
||||
Provider provider);
|
||||
|
||||
Task UpdateSeatMinimums(
|
||||
Provider provider,
|
||||
int enterpriseSeatMinimum,
|
||||
int teamsSeatMinimum);
|
||||
/// <summary>
|
||||
/// Changes the assigned provider plan for the provider.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to change the provider plan.</param>
|
||||
/// <returns></returns>
|
||||
Task ChangePlan(ChangeProviderPlanCommand command);
|
||||
|
||||
Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
|
||||
}
|
||||
|
@ -106,7 +106,6 @@ public static class FeatureFlagKeys
|
||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||
public const string ItemShare = "item-share";
|
||||
public const string DuoRedirect = "duo-redirect";
|
||||
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
|
||||
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
||||
|
@ -14,6 +14,17 @@ public interface IStripeAdapter
|
||||
CustomerBalanceTransactionCreateOptions options);
|
||||
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
||||
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<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
||||
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
||||
|
@ -81,6 +81,20 @@ public class StripeAdapter : IStripeAdapter
|
||||
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,
|
||||
Stripe.SubscriptionUpdateOptions options = null)
|
||||
{
|
||||
|
@ -792,19 +792,16 @@ public class StripePaymentService : IPaymentService
|
||||
var daysUntilDue = sub.DaysUntilDue;
|
||||
var chargeNow = collectionMethod == "charge_automatically";
|
||||
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
||||
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
|
||||
var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
|
||||
|
||||
var subUpdateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = updatedItemOptions,
|
||||
ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow
|
||||
? Constants.AlwaysInvoice
|
||||
: Constants.CreateProrations,
|
||||
ProrationBehavior = invoiceNow ? Constants.AlwaysInvoice : Constants.CreateProrations,
|
||||
DaysUntilDue = daysUntilDue ?? 1,
|
||||
CollectionMethod = "send_invoice"
|
||||
};
|
||||
if (!invoiceNow && isAnnualPlan && isPm5864DollarThresholdEnabled && sub.Status.Trim() != "trialing")
|
||||
if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != "trialing")
|
||||
{
|
||||
subUpdateOptions.PendingInvoiceItemInterval =
|
||||
new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" };
|
||||
@ -838,7 +835,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!isPm5864DollarThresholdEnabled && !invoiceNow)
|
||||
if (invoiceNow)
|
||||
{
|
||||
if (chargeNow)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user