1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 23:52:50 -05:00

Organization autoscaling (#1585)

* Add autoscale fields to Organization

* Add autoscale setting changes

* Autoscale organizations

updates InviteUsersAsync to support all invite sources.

sends an email to org owners when organization autoscaled

* All organizations autoscale

Disabling autoscaling can be done by setting max seats to current seats.

We only warn about autoscaling on the first autoscaling event.

* Fix tests

* Bug fixes

* Simplify subscription update logic

* Void invoices that fail to delete

Stripe no longer allows deletion of draft invoices that were created as part of subscription updates. It's necessary to void out these invoices without sending tem to the client.

* Notify org owners when their subscription runs out of seats

* Use datetime for notifications

Allows for later re-sending email if we want to periodically remind
owners

* Do not update subscription if it already matches new quatity

* Include all migrations

* Remove unnecessary inline styling

* SubscriptionUpdate handles update decisions

* Remove unnecessary html setter

* PR review

* Use minimum access for class methods
This commit is contained in:
Matt Gibson
2021-09-23 06:36:08 -04:00
committed by GitHub
parent c2d5106a4d
commit d39f45c81c
46 changed files with 4113 additions and 231 deletions

View File

@ -40,6 +40,7 @@ namespace Bit.Core.Models.Api
public string BillingAddressPostalCode { get; set; }
[StringLength(2)]
public string BillingAddressCountry { get; set; }
public int? MaxAutoscaleSeats { get; set; }
public virtual OrganizationSignup ToOrganizationSignup(User user)
{
@ -52,6 +53,7 @@ namespace Bit.Core.Models.Api
PaymentMethodType = PaymentMethodType,
PaymentToken = PaymentToken,
AdditionalSeats = AdditionalSeats,
MaxAutoscaleSeats = MaxAutoscaleSeats,
AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(0),
PremiumAccessAddon = PremiumAccessAddon,
BillingEmail = BillingEmail,

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class OrganizationSubscriptionUpdateRequestModel
{
[Required]
public int SeatAdjustment { get; set; }
public int? MaxAutoscaleSeats { get; set; }
}
}

View File

@ -30,6 +30,7 @@ namespace Bit.Core.Models.Api
Plan = new PlanResponseModel(Utilities.StaticStore.Plans.FirstOrDefault(plan => plan.Type == organization.PlanType));
PlanType = organization.PlanType;
Seats = organization.Seats;
MaxAutoscaleSeats = organization.MaxAutoscaleSeats;
MaxCollections = organization.MaxCollections;
MaxStorageGb = organization.MaxStorageGb;
UsePolicies = organization.UsePolicies;
@ -59,6 +60,7 @@ namespace Bit.Core.Models.Api
public PlanResponseModel Plan { get; set; }
public PlanType PlanType { get; set; }
public int? Seats { get; set; }
public int? MaxAutoscaleSeats { get; set; } = null;
public short? MaxCollections { get; set; }
public short? MaxStorageGb { get; set; }
public bool UsePolicies { get; set; }

View File

@ -177,7 +177,7 @@ namespace Bit.Core.Models.Business
}
}
public bool CanUse(GlobalSettings globalSettings)
public bool CanUse(IGlobalSettings globalSettings)
{
if (!Enabled || Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)
{

View File

@ -12,5 +12,6 @@ namespace Bit.Core.Models.Business
public string CollectionName { get; set; }
public PaymentMethodType? PaymentMethodType { get; set; }
public string PaymentToken { get; set; }
public int? MaxAutoscaleSeats { get; set; } = null;
}
}

View File

@ -1,7 +1,6 @@
using System.Linq;
using Bit.Core.Models.Table;
using Stripe;
using StaticStore = Bit.Core.Models.StaticStore;
namespace Bit.Core.Models.Business
{
@ -11,6 +10,10 @@ namespace Bit.Core.Models.Business
public abstract SubscriptionItemOptions RevertItemOptions(Subscription subscription);
public abstract SubscriptionItemOptions UpgradeItemOptions(Subscription subscription);
public bool UpdateNeeded(Subscription subscription) =>
(SubscriptionItem(subscription)?.Quantity ?? 0) != (UpgradeItemOptions(subscription).Quantity ?? 0);
protected SubscriptionItem SubscriptionItem(Subscription subscription) =>
subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == PlanId);
}

View File

@ -0,0 +1,11 @@
using System;
namespace Bit.Core.Models.Mail
{
public class OrganizationSeatsAutoscaledViewModel : BaseMailModel
{
public Guid OrganizationId { get; set; }
public int InitialSeatCount { get; set; }
public int CurrentSeatCount { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Bit.Core.Models.Mail
{
public class OrganizationSeatsMaxReachedViewModel : BaseMailModel
{
public Guid OrganizationId { get; set; }
public int MaxSeatCount { get; set; }
}
}

View File

@ -15,6 +15,7 @@ namespace Bit.Core.Models.StaticStore
public short? BaseStorageGb { get; set; }
public short? MaxCollections { get; set; }
public short? MaxUsers { get; set; }
public bool AllowSeatAutoscale { get; set; }
public bool HasAdditionalSeatsOption { get; set; }
public int? MaxAdditionalSeats { get; set; }

View File

@ -66,6 +66,8 @@ namespace Bit.Core.Models.Table
public DateTime? ExpirationDate { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
public int? MaxAutoscaleSeats { get; set; } = null;
public DateTime? OwnersNotifiedOfAutoscaling { get; set; } = null;
public void SetNewId()
{