mirror of
https://github.com/bitwarden/server.git
synced 2025-07-18 16:11:28 -05:00
[PM 20621]Update error message when lowering seat count (#5836)
* implement the seat decrease error message * Resolve the comment regarding abstraction * Resolved the database failure Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Resolve the failing test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Resolve the failing test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Resolve the failing upgrade test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Resolve the failing test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Resolve the failing test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Removed the unused method * Remove the total calculation from the stored procedure * Refactoring base on pr feedback * Refactoring base on pr feedback * Resolve the fauiling database * Resolve the failing database test * Resolve the database test * Remove duplicate migrations * resolve the failing test * Removed the unneeded change * remove this file * Reverted Deleted migration * revert the added space * resolve the stored procedure name * Rename the migration name * Updated the stored procedure name * Revert the changes on the sproc * Revert unrelated changes * Remove the unused method * improved the xmldoc * Add an integration testing * Add the use of helper test class Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Resolve the failing test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Resolve the failing test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * remove object look up * Resolve message rollback Signed-off-by: Cy Okeke <cokeke@bitwarden.com> --------- Signed-off-by: Cy Okeke <cokeke@bitwarden.com>
This commit is contained in:
@ -87,7 +87,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
InviteOrganization = request.InviteOrganization,
|
||||
PerformedBy = request.PerformedBy,
|
||||
PerformedAt = request.PerformedAt,
|
||||
OccupiedPmSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId),
|
||||
OccupiedPmSeats = (await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)).Total,
|
||||
OccupiedSmSeats = await organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)
|
||||
});
|
||||
|
||||
|
@ -70,8 +70,8 @@ public class RestoreOrganizationUserCommand(
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;
|
||||
|
||||
if (availableSeats < 1)
|
||||
{
|
||||
@ -163,8 +163,8 @@ public class RestoreOrganizationUserCommand(
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;
|
||||
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
||||
await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired);
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -25,4 +26,14 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
||||
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||
Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);
|
||||
Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of occupied seats for an organization.
|
||||
/// OrganizationUsers occupy a seat, unless they are revoked.
|
||||
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
|
||||
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
|
||||
/// <returns>The number of occupied seats for the organization.</returns>
|
||||
Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||
}
|
||||
|
@ -18,16 +18,6 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
|
||||
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
|
||||
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of occupied seats for an organization.
|
||||
/// Occupied seats are OrganizationUsers that have at least been invited.
|
||||
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
|
||||
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
|
||||
/// <returns>The number of occupied seats for the organization.</returns>
|
||||
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
|
||||
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
||||
|
@ -294,11 +294,20 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
||||
{
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
if (occupiedSeats > newSeatTotal)
|
||||
var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
if (seatCounts.Total > newSeatTotal)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
||||
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
|
||||
if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0)
|
||||
{
|
||||
throw new BadRequestException($"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
|
||||
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {seatCounts.Total} seats filled. " +
|
||||
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -726,8 +735,8 @@ public class OrganizationService : IOrganizationService
|
||||
var newSeatsRequired = 0;
|
||||
if (organization.Seats.HasValue)
|
||||
{
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.Value - occupiedSeats;
|
||||
var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.Value - seatCounts.Total;
|
||||
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
|
||||
}
|
||||
|
||||
@ -1177,8 +1186,8 @@ public class OrganizationService : IOrganizationService
|
||||
var enoughSeatsAvailable = true;
|
||||
if (organization.Seats.HasValue)
|
||||
{
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
seatsAvailable = organization.Seats.Value - occupiedSeats;
|
||||
var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
seatsAvailable = organization.Seats.Value - seatCounts.Total;
|
||||
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,6 @@ public class OrganizationBillingService(
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<OrganizationBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IPricingClient pricingClient,
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
@ -78,14 +77,14 @@ public class OrganizationBillingService(
|
||||
var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization);
|
||||
|
||||
var isManaged = organization.Status == OrganizationStatusType.Managed;
|
||||
var orgOccupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||
{
|
||||
return OrganizationMetadata.Default with
|
||||
{
|
||||
IsEligibleForSelfHost = isEligibleForSelfHost,
|
||||
IsManaged = isManaged,
|
||||
OrganizationOccupiedSeats = orgOccupiedSeats
|
||||
OrganizationOccupiedSeats = orgOccupiedSeats.Total
|
||||
};
|
||||
}
|
||||
|
||||
@ -120,7 +119,7 @@ public class OrganizationBillingService(
|
||||
invoice?.DueDate,
|
||||
invoice?.Created,
|
||||
subscription.CurrentPeriodEnd,
|
||||
orgOccupiedSeats);
|
||||
orgOccupiedSeats.Total);
|
||||
}
|
||||
|
||||
public async Task
|
||||
|
@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationSeatCounts
|
||||
{
|
||||
public int Users { get; set; }
|
||||
public int Sponsored { get; set; }
|
||||
public int Total => Users + Sponsored;
|
||||
}
|
@ -16,7 +16,7 @@ public class CreateSponsorshipCommand(
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
IUserService userService,
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationUserRepository organizationUserRepository) : ICreateSponsorshipCommand
|
||||
IOrganizationRepository organizationRepository) : ICreateSponsorshipCommand
|
||||
{
|
||||
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
|
||||
Organization sponsoringOrganization,
|
||||
@ -89,8 +89,8 @@ public class CreateSponsorshipCommand(
|
||||
|
||||
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
|
||||
{
|
||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id);
|
||||
var availableSeats = sponsoringOrganization.Seats.Value - occupiedSeats;
|
||||
var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id);
|
||||
var availableSeats = sponsoringOrganization.Seats.Value - seatCounts.Total;
|
||||
|
||||
if (availableSeats <= 0)
|
||||
{
|
||||
|
@ -107,12 +107,20 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
(newPlan.PasswordManager.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
||||
if (!organization.Seats.HasValue || organization.Seats.Value > updatedPasswordManagerSeats)
|
||||
{
|
||||
var occupiedSeats =
|
||||
await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
if (occupiedSeats > updatedPasswordManagerSeats)
|
||||
var seatCounts =
|
||||
await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
if (seatCounts.Total > updatedPasswordManagerSeats)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
||||
if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0)
|
||||
{
|
||||
throw new BadRequestException($"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
|
||||
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {seatCounts.Total} seats filled. " +
|
||||
$"Your new plan only has ({updatedPasswordManagerSeats}) seats. Remove some users.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user