From cedbea4a6075fab11f140f717f1a2ced6ba283b4 Mon Sep 17 00:00:00 2001
From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Date: Thu, 21 Dec 2023 22:10:14 +0100
Subject: [PATCH] [AC-85] Set Max Seats Autoscale and Current Seats via Public
 API (#3389)

* Add new public models and controllers

* Resolve pr comments

* Fix the failing test

* Change the controller name

* resolve pr comments

* add the IValidatableObject

* resolve pr comment

* resolve pr comments

* resolve pr comments

* resolve

* removing the whitespaces

* code refactoring
---
 .../Controllers/OrganizationController.cs     |  81 ++++++++++
 ...anizationSubscriptionUpdateRequestModel.cs | 138 ++++++++++++++++++
 2 files changed, 219 insertions(+)
 create mode 100644 src/Api/Billing/Public/Controllers/OrganizationController.cs
 create mode 100644 src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs

diff --git a/src/Api/Billing/Public/Controllers/OrganizationController.cs b/src/Api/Billing/Public/Controllers/OrganizationController.cs
new file mode 100644
index 0000000000..294165a7e8
--- /dev/null
+++ b/src/Api/Billing/Public/Controllers/OrganizationController.cs
@@ -0,0 +1,81 @@
+using System.Net;
+using Bit.Api.Models.Public.Response;
+using Bit.Core.Context;
+using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
+using Bit.Core.Utilities;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using OrganizationSubscriptionUpdateRequestModel = Bit.Api.Billing.Public.Models.OrganizationSubscriptionUpdateRequestModel;
+
+namespace Bit.Api.Billing.Public.Controllers;
+
+[Route("public/organization")]
+[Authorize("Organization")]
+public class OrganizationController : Controller
+{
+    private readonly IOrganizationService _organizationService;
+    private readonly ICurrentContext _currentContext;
+    private readonly IOrganizationRepository _organizationRepository;
+    private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
+
+    public OrganizationController(
+        IOrganizationService organizationService,
+        ICurrentContext currentContext,
+        IOrganizationRepository organizationRepository,
+        IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand)
+    {
+        _organizationService = organizationService;
+        _currentContext = currentContext;
+        _organizationRepository = organizationRepository;
+        _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
+    }
+
+    /// <summary>
+    /// Update the organization's current subscription for Password Manager and/or Secrets Manager.
+    /// </summary>
+    /// <param name="model">The request model containing the updated subscription information.</param>
+    [HttpPut("subscription")]
+    [SelfHosted(NotSelfHostedOnly = true)]
+    [ProducesResponseType((int)HttpStatusCode.OK)]
+    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
+    [ProducesResponseType((int)HttpStatusCode.NotFound)]
+    public async Task<IActionResult> PostSubscriptionAsync([FromBody] OrganizationSubscriptionUpdateRequestModel model)
+    {
+
+        await UpdatePasswordManagerAsync(model, _currentContext.OrganizationId.Value);
+
+        await UpdateSecretsManagerAsync(model, _currentContext.OrganizationId.Value);
+
+        return new OkResult();
+    }
+
+    private async Task UpdatePasswordManagerAsync(OrganizationSubscriptionUpdateRequestModel model, Guid organizationId)
+    {
+        if (model.PasswordManager != null)
+        {
+            var organization = await _organizationRepository.GetByIdAsync(organizationId);
+
+            model.PasswordManager.ToPasswordManagerSubscriptionUpdate(organization);
+            await _organizationService.UpdateSubscription(organization.Id, (int)model.PasswordManager.Seats,
+                model.PasswordManager.MaxAutoScaleSeats);
+            if (model.PasswordManager.Storage.HasValue)
+            {
+                await _organizationService.AdjustStorageAsync(organization.Id, (short)model.PasswordManager.Storage);
+            }
+        }
+    }
+
+    private async Task UpdateSecretsManagerAsync(OrganizationSubscriptionUpdateRequestModel model, Guid organizationId)
+    {
+        if (model.SecretsManager != null)
+        {
+            var organization =
+                await _organizationRepository.GetByIdAsync(organizationId);
+
+            var organizationUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization);
+            await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);
+        }
+    }
+}
diff --git a/src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs b/src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs
new file mode 100644
index 0000000000..781ad3ca53
--- /dev/null
+++ b/src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs
@@ -0,0 +1,138 @@
+using System.ComponentModel.DataAnnotations;
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.Models.Business;
+
+namespace Bit.Api.Billing.Public.Models;
+
+public class OrganizationSubscriptionUpdateRequestModel : IValidatableObject
+{
+    public PasswordManagerSubscriptionUpdateModel PasswordManager { get; set; }
+    public SecretsManagerSubscriptionUpdateModel SecretsManager { get; set; }
+
+    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
+    {
+        if (PasswordManager == null && SecretsManager == null)
+        {
+            yield return new ValidationResult("At least one of PasswordManager or SecretsManager must be provided.");
+        }
+
+        yield return ValidationResult.Success;
+    }
+}
+
+public class PasswordManagerSubscriptionUpdateModel
+{
+    public int? Seats { get; set; }
+    public int? Storage { get; set; }
+    private int? _maxAutoScaleSeats;
+    public int? MaxAutoScaleSeats
+    {
+        get { return _maxAutoScaleSeats; }
+        set { _maxAutoScaleSeats = value < 0 ? null : value; }
+    }
+
+    public virtual void ToPasswordManagerSubscriptionUpdate(Organization organization)
+    {
+        UpdateMaxAutoScaleSeats(organization);
+
+        UpdateSeats(organization);
+
+        UpdateStorage(organization);
+    }
+
+    private void UpdateMaxAutoScaleSeats(Organization organization)
+    {
+        MaxAutoScaleSeats ??= organization.MaxAutoscaleSeats;
+    }
+
+    private void UpdateSeats(Organization organization)
+    {
+        if (Seats is > 0)
+        {
+            if (organization.Seats.HasValue)
+            {
+                Seats = Seats.Value - organization.Seats.Value;
+            }
+        }
+        else
+        {
+            Seats = 0;
+        }
+    }
+
+    private void UpdateStorage(Organization organization)
+    {
+        if (Storage is > 0)
+        {
+            if (organization.MaxStorageGb.HasValue)
+            {
+                Storage = (short?)(Storage - organization.MaxStorageGb.Value);
+            }
+        }
+        else
+        {
+            Storage = null;
+        }
+    }
+}
+
+public class SecretsManagerSubscriptionUpdateModel
+{
+    public int? Seats { get; set; }
+    private int? _maxAutoScaleSeats;
+    public int? MaxAutoScaleSeats
+    {
+        get { return _maxAutoScaleSeats; }
+        set { _maxAutoScaleSeats = value < 0 ? null : value; }
+    }
+    public int? ServiceAccounts { get; set; }
+    private int? _maxAutoScaleServiceAccounts;
+    public int? MaxAutoScaleServiceAccounts
+    {
+        get { return _maxAutoScaleServiceAccounts; }
+        set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; }
+    }
+
+    public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization)
+    {
+        var update = UpdateUpdateMaxAutoScale(organization);
+        UpdateSeats(organization, update);
+        UpdateServiceAccounts(organization, update);
+        return update;
+    }
+
+    private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization)
+    {
+        var update = new SecretsManagerSubscriptionUpdate(organization, false)
+        {
+            MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats,
+            MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts
+        };
+        return update;
+    }
+
+    private void UpdateSeats(Organization organization, SecretsManagerSubscriptionUpdate update)
+    {
+        if (Seats is > 0)
+        {
+            if (organization.SmSeats.HasValue)
+            {
+                Seats = Seats.Value - organization.SmSeats.Value;
+
+            }
+            update.AdjustSeats(Seats.Value);
+        }
+    }
+
+    private void UpdateServiceAccounts(Organization organization, SecretsManagerSubscriptionUpdate update)
+    {
+        if (ServiceAccounts is > 0)
+        {
+            if (organization.SmServiceAccounts.HasValue)
+            {
+                ServiceAccounts = ServiceAccounts.Value - organization.SmServiceAccounts.Value;
+            }
+            update.AdjustServiceAccounts(ServiceAccounts.Value);
+        }
+    }
+}