diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs index e7b637eb32..f409fdc30d 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs @@ -1,95 +1,48 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Extensions; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SponsorshipCreation; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; -public class CreateSponsorshipCommand( - IOrganizationSponsorshipRepository organizationSponsorshipRepository, - IUserService userService, - ICurrentContext currentContext) - : ICreateSponsorshipCommand +public class CreateSponsorshipCommand : ICreateSponsorshipCommand { + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + + private readonly BaseCreateSponsorshipHandler _createSponsorshipHandler; + + public CreateSponsorshipCommand( + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IUserService userService, + ICurrentContext currentContext) + { + _organizationSponsorshipRepository = organizationSponsorshipRepository; + + var adminInitiatedSponsorshipHandler = new CreateAdminInitiatedSponsorshipHandler(currentContext); + _createSponsorshipHandler = new CreateSponsorshipHandler(userService, organizationSponsorshipRepository); + _createSponsorshipHandler.SetNext(adminInitiatedSponsorshipHandler); + } + public async Task CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName) { - var sponsoringUser = await userService.GetUserByIdAsync(sponsoringOrgUser.UserId.Value); - if (sponsoringUser == null || string.Equals(sponsoringUser.Email, sponsoredEmail, System.StringComparison.InvariantCultureIgnoreCase)) - { - throw new BadRequestException("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email."); - } - - var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductTierType; - var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier(); - - if (requiredSponsoringProductType == null || - sponsoringOrgProductTier != requiredSponsoringProductType.Value) - { - throw new BadRequestException("Specified Organization cannot sponsor other organizations."); - } - - if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed) - { - throw new BadRequestException("Only confirmed users can sponsor other organizations."); - } - - var isAdminInitiated = false; - if (currentContext.UserId != sponsoringOrgUser.UserId) - { - var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrg.Id); - OrganizationUserType[] allowedUserTypes = - [ - OrganizationUserType.Admin, - OrganizationUserType.Owner, - OrganizationUserType.Custom - ]; - if (!organization.Permissions.ManageUsers || allowedUserTypes.All(x => x != organization.Type)) - { - throw new UnauthorizedAccessException("You do not have permissions to send sponsorships on behalf of the organization."); - } - isAdminInitiated = true; - } - - var existingOrgSponsorship = await organizationSponsorshipRepository - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id); - if (existingOrgSponsorship?.SponsoredOrganizationId != null) - { - throw new BadRequestException("Can only sponsor one organization per Organization User."); - } - - var sponsorship = new OrganizationSponsorship - { - SponsoringOrganizationId = sponsoringOrg.Id, - SponsoringOrganizationUserId = sponsoringOrgUser.Id, - FriendlyName = friendlyName, - OfferedToEmail = sponsoredEmail, - PlanSponsorshipType = sponsorshipType, - IsAdminInitiated = isAdminInitiated - }; - - if (existingOrgSponsorship != null) - { - // Replace existing invalid offer with our new sponsorship offer - sponsorship.Id = existingOrgSponsorship.Id; - } + var createSponsorshipRequest = new CreateSponsorshipRequest(sponsoringOrg, sponsoringOrgUser, sponsorshipType, sponsoredEmail, friendlyName); + var sponsorship = await _createSponsorshipHandler.HandleAsync(createSponsorshipRequest); try { - await organizationSponsorshipRepository.UpsertAsync(sponsorship); + await _organizationSponsorshipRepository.UpsertAsync(sponsorship); return sponsorship; } catch { - if (sponsorship.Id != default) + if (sponsorship.Id != Guid.Empty) { - await organizationSponsorshipRepository.DeleteAsync(sponsorship); + await _organizationSponsorshipRepository.DeleteAsync(sponsorship); } throw; } diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/BaseCreateSponsorshipHandler.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/BaseCreateSponsorshipHandler.cs new file mode 100644 index 0000000000..5570a46f53 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/BaseCreateSponsorshipHandler.cs @@ -0,0 +1,23 @@ +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SponsorshipCreation; + +public abstract class BaseCreateSponsorshipHandler +{ + private BaseCreateSponsorshipHandler _next; + + public BaseCreateSponsorshipHandler SetNext(BaseCreateSponsorshipHandler next) + { + _next = next; + return next; + } + + public virtual async Task HandleAsync(CreateSponsorshipRequest request) + { + if (_next != null) + { + return await _next.HandleAsync(request); + } + return null; + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/CreateAdminInitiatedSponsorshipHandler.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/CreateAdminInitiatedSponsorshipHandler.cs new file mode 100644 index 0000000000..6d2d0c61f7 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/CreateAdminInitiatedSponsorshipHandler.cs @@ -0,0 +1,42 @@ +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SponsorshipCreation; + +public class CreateAdminInitiatedSponsorshipHandler( + ICurrentContext currentContext) : BaseCreateSponsorshipHandler +{ + public override async Task HandleAsync(CreateSponsorshipRequest request) + { + var isAdminInitiated = false; + if (currentContext.UserId != request.SponsoringMember.UserId) + { + var organization = currentContext.Organizations.First(x => x.Id == request.SponsoringOrganization.Id); + OrganizationUserType[] allowedUserTypes = + [ + OrganizationUserType.Admin, + OrganizationUserType.Owner, + OrganizationUserType.Custom + ]; + if (!organization.Permissions.ManageUsers || allowedUserTypes.All(x => x != organization.Type)) + { + throw new UnauthorizedAccessException("You do not have permissions to send sponsorships on behalf of the organization."); + } + + if (!request.SponsoringOrganization.UseAdminSponsoredFamilies) + { + throw new BadRequestException("Sponsoring organization cannot sponsor other Family organizations."); + } + + isAdminInitiated = true; + } + + var sponsorship = await base.HandleAsync(request) ?? new OrganizationSponsorship(); + + sponsorship.IsAdminInitiated = isAdminInitiated; + + return sponsorship; + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/CreateSponsorshipHandler.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/CreateSponsorshipHandler.cs new file mode 100644 index 0000000000..5ef753e445 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/CreateSponsorshipHandler.cs @@ -0,0 +1,61 @@ +using Bit.Core.Billing.Extensions; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SponsorshipCreation; + +public class CreateSponsorshipHandler( + IUserService userService, + IOrganizationSponsorshipRepository organizationSponsorshipRepository) : BaseCreateSponsorshipHandler +{ + public override async Task HandleAsync(CreateSponsorshipRequest request) + { + var sponsoringUser = await userService.GetUserByIdAsync(request.SponsoringMember.UserId.Value); + + if (sponsoringUser == null || string.Equals(sponsoringUser.Email, request.SponsoredEmail, System.StringComparison.InvariantCultureIgnoreCase)) + { + throw new BadRequestException("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email."); + } + + var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(request.SponsorshipType)?.SponsoringProductTierType; + var sponsoringOrgProductTier = request.SponsoringOrganization.PlanType.GetProductTier(); + + if (requiredSponsoringProductType == null || + sponsoringOrgProductTier != requiredSponsoringProductType.Value) + { + throw new BadRequestException("Specified Organization cannot sponsor other organizations."); + } + + if (request.SponsoringMember == null || request.SponsoringMember.Status != OrganizationUserStatusType.Confirmed) + { + throw new BadRequestException("Only confirmed users can sponsor other organizations."); + } + + var existingOrgSponsorship = await organizationSponsorshipRepository + .GetBySponsoringOrganizationUserIdAsync(request.SponsoringMember.Id); + if (existingOrgSponsorship?.SponsoredOrganizationId != null) + { + throw new BadRequestException("Can only sponsor one organization per Organization User."); + } + + var sponsorship = await base.HandleAsync(request) ?? new OrganizationSponsorship(); + + sponsorship.SponsoringOrganizationId = request.SponsoringOrganization.Id; + sponsorship.SponsoringOrganizationUserId = request.SponsoringMember.Id; + sponsorship.FriendlyName = request.FriendlyName; + sponsorship.OfferedToEmail = request.SponsoredEmail; + sponsorship.PlanSponsorshipType = request.SponsorshipType; + + if (existingOrgSponsorship != null) + { + // Replace existing invalid offer with our new sponsorship offer + sponsorship.Id = existingOrgSponsorship.Id; + } + + return sponsorship; + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/CreateSponsorshipRequest.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/CreateSponsorshipRequest.cs new file mode 100644 index 0000000000..a989474106 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SponsorshipCreation/CreateSponsorshipRequest.cs @@ -0,0 +1,12 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SponsorshipCreation; + +public record CreateSponsorshipRequest( + Organization SponsoringOrganization, + OrganizationUser SponsoringMember, + PlanSponsorshipType SponsorshipType, + string SponsoredEmail, + string FriendlyName);