diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 6e0706278b..fd6f3fa284 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -128,6 +128,45 @@ namespace Bit.Api.Controllers await _organizationService.ReplacePaymentMethodAsync(orgIdGuid, model.PaymentToken); } + [HttpPut("{id}/upgrade")] + [HttpPost("{id}/upgrade")] + public async Task PutUpgrade(string id, [FromBody]OrganizationUpgradeRequestModel model) + { + var orgIdGuid = new Guid(id); + if(!_currentContext.OrganizationOwner(orgIdGuid)) + { + throw new NotFoundException(); + } + + await _organizationService.UpgradePlanAsync(orgIdGuid, model.PlanType, model.AdditionalSeats); + } + + [HttpPut("{id}/seat")] + [HttpPost("{id}/seat")] + public async Task PutSeat(string id, [FromBody]OrganizationSeatRequestModel model) + { + var orgIdGuid = new Guid(id); + if(!_currentContext.OrganizationOwner(orgIdGuid)) + { + throw new NotFoundException(); + } + + await _organizationService.AdjustSeatsAsync(orgIdGuid, model.SeatAdjustment.Value); + } + + [HttpPut("{id}/cancel")] + [HttpPost("{id}/cancel")] + public async Task PutCancel(string id, [FromBody]OrganizationSeatRequestModel model) + { + var orgIdGuid = new Guid(id); + if(!_currentContext.OrganizationOwner(orgIdGuid)) + { + throw new NotFoundException(); + } + + await _organizationService.CancelSubscriptionAsync(orgIdGuid, true); + } + [HttpDelete("{id}")] [HttpPost("{id}/delete")] public async Task Delete(string id) diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationSeatRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationSeatRequestModel.cs new file mode 100644 index 0000000000..055a331cdc --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/OrganizationSeatRequestModel.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class OrganizationSeatRequestModel + { + [Required] + public int? SeatAdjustment { get; set; } + } +} diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationUpgradeRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationUpgradeRequestModel.cs new file mode 100644 index 0000000000..63346e582d --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/OrganizationUpgradeRequestModel.cs @@ -0,0 +1,12 @@ +using Bit.Core.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class OrganizationUpgradeRequestModel + { + public PlanType PlanType { get; set; } + [Range(0, double.MaxValue)] + public short AdditionalSeats { get; set; } + } +} diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index 2df9a946ce..557f8f084f 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -3,6 +3,7 @@ using Bit.Core.Models.Business; using Bit.Core.Models.Table; using System; using System.Collections.Generic; +using Bit.Core.Enums; namespace Bit.Core.Services { @@ -11,6 +12,8 @@ namespace Bit.Core.Services Task GetBillingAsync(Organization organization); Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken); Task CancelSubscriptionAsync(Guid organizationId, bool endOfPeriod = false); + Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats); + Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task> SignUpAsync(OrganizationSignup organizationSignup); Task InviteUserAsync(Guid organizationId, Guid invitingUserId, string email, Enums.OrganizationUserType type, IEnumerable subvaults); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index de3a5a7321..673617b22c 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -175,7 +175,7 @@ namespace Bit.Core.Services } } - public async Task UpgradePlanAsync(Guid organizationId, PlanType plan, short additionalSeats) + public async Task UpgradePlanAsync(Guid organizationId, PlanType plan, int additionalSeats) { var organization = await _organizationRepository.GetByIdAsync(organizationId); if(organization == null) @@ -300,7 +300,7 @@ namespace Bit.Core.Services } } - public async Task AdjustAdditionalSeatsAsync(Guid organizationId, short additionalSeats) + public async Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment) { var organization = await _organizationRepository.GetByIdAsync(organizationId); if(organization == null) @@ -329,20 +329,26 @@ namespace Bit.Core.Services throw new BadRequestException("Plan does not allow additional seats."); } + var newSeatTotal = organization.Seats + seatAdjustment; + if(plan.BaseSeats > newSeatTotal) + { + throw new BadRequestException($"Plan has a minimum of {plan.BaseSeats} seats."); + } + + var additionalSeats = newSeatTotal - plan.BaseSeats; if(plan.MaxAdditionalSeats.HasValue && additionalSeats > plan.MaxAdditionalSeats.Value) { throw new BadRequestException($"Organization plan allows a maximum of " + $"{plan.MaxAdditionalSeats.Value} additional seats."); } - var planNewSeats = (short)(plan.BaseSeats + additionalSeats); - if(!organization.Seats.HasValue || organization.Seats.Value > planNewSeats) + if(!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal) { var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id); - if(userCount >= planNewSeats) + if(userCount > newSeatTotal) { throw new BadRequestException($"Your organization currently has {userCount} seats filled. Your new plan " + - $"only has ({planNewSeats}) seats. Remove some users."); + $"only has ({newSeatTotal}) seats. Remove some users."); } }