mirror of
https://github.com/bitwarden/server.git
synced 2025-02-17 02:01:53 +01:00
Billing updates
- Break monthly and annual plans into two. - Add upgrade and adjust additional users
This commit is contained in:
parent
52dcd6d6ab
commit
bb0555a6d9
@ -3,9 +3,11 @@
|
||||
public enum PlanType : byte
|
||||
{
|
||||
Free = 0,
|
||||
Personal = 1,
|
||||
Teams = 2,
|
||||
Enterprise = 3,
|
||||
Custom = 4
|
||||
PersonalAnnually = 1,
|
||||
TeamsMonthly = 2,
|
||||
TeamsAnnually = 3,
|
||||
EnterpriseMonthly = 4,
|
||||
EnterpriseAnnually = 5,
|
||||
Custom = 6
|
||||
}
|
||||
}
|
||||
|
13
src/Core/Models/Business/OrganizationChangePlan.cs
Normal file
13
src/Core/Models/Business/OrganizationChangePlan.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
|
||||
namespace Bit.Core.Models.Business
|
||||
{
|
||||
public class OrganizationChangePlan
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public PlanType PlanType { get; set; }
|
||||
public short AdditionalUsers { get; set; }
|
||||
public bool Monthly { get; set; }
|
||||
}
|
||||
}
|
@ -1,25 +1,20 @@
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
|
||||
namespace Bit.Core.Models.StaticStore
|
||||
{
|
||||
public class Plan
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string StripeAnnualPlanId { get; set; }
|
||||
public string StripeAnnualUserPlanId { get; set; }
|
||||
public string StripeMonthlyPlanId { get; set; }
|
||||
public string StripeMonthlyUserPlanId { get; set; }
|
||||
public string StripePlanId { get; set; }
|
||||
public string StripeUserPlanId { get; set; }
|
||||
public PlanType Type { get; set; }
|
||||
public short BaseUsers { get; set; }
|
||||
public bool CanBuyAdditionalUsers { get; set; }
|
||||
public short? MaxAdditionalUsers { get; set; }
|
||||
public bool CanMonthly { get; set; }
|
||||
public decimal BaseMonthlyPrice { get; set; }
|
||||
public decimal UserMonthlyPrice { get; set; }
|
||||
public decimal BaseAnnualPrice { get; set; }
|
||||
public decimal UserAnnualPrice { get; set; }
|
||||
public decimal BasePrice { get; set; }
|
||||
public decimal UserPrice { get; set; }
|
||||
public short? MaxSubvaults { get; set; }
|
||||
public int UpgradeSortOrder { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -160,6 +160,201 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpgradePlanAsync(OrganizationChangePlan model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(model.OrganizationId);
|
||||
if(organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(organization.StripeCustomerId))
|
||||
{
|
||||
throw new BadRequestException("No payment method found.");
|
||||
}
|
||||
|
||||
var existingPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
if(existingPlan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
}
|
||||
|
||||
var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == model.PlanType && !p.Disabled);
|
||||
if(newPlan == null)
|
||||
{
|
||||
throw new BadRequestException("Plan not found.");
|
||||
}
|
||||
|
||||
if(existingPlan.Type == newPlan.Type)
|
||||
{
|
||||
throw new BadRequestException("Organization is already on this plan.");
|
||||
}
|
||||
|
||||
if(existingPlan.UpgradeSortOrder >= newPlan.UpgradeSortOrder)
|
||||
{
|
||||
throw new BadRequestException("You cannot upgrade to this plan.");
|
||||
}
|
||||
|
||||
if(!newPlan.CanBuyAdditionalUsers && model.AdditionalUsers > 0)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional users.");
|
||||
}
|
||||
|
||||
if(newPlan.CanBuyAdditionalUsers && newPlan.MaxAdditionalUsers.HasValue &&
|
||||
model.AdditionalUsers > newPlan.MaxAdditionalUsers.Value)
|
||||
{
|
||||
throw new BadRequestException($"Selected plan allows a maximum of " +
|
||||
$"{newPlan.MaxAdditionalUsers.Value} additional users.");
|
||||
}
|
||||
|
||||
var newPlanMaxUsers = (short)(newPlan.BaseUsers + (newPlan.CanBuyAdditionalUsers ? model.AdditionalUsers : 0));
|
||||
if(!organization.MaxUsers.HasValue || organization.MaxUsers.Value > newPlanMaxUsers)
|
||||
{
|
||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
||||
if(userCount >= newPlanMaxUsers)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {userCount} users. Your new plan " +
|
||||
$"allows for a maximum of ({newPlanMaxUsers}) users. Remove some users.");
|
||||
}
|
||||
}
|
||||
|
||||
if(newPlan.MaxSubvaults.HasValue &&
|
||||
(!organization.MaxSubvaults.HasValue || organization.MaxSubvaults.Value > newPlan.MaxSubvaults.Value))
|
||||
{
|
||||
var subvaultCount = await _subvaultRepository.GetCountByOrganizationIdAsync(organization.Id);
|
||||
if(subvaultCount > newPlan.MaxSubvaults.Value)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {subvaultCount} subvaults. " +
|
||||
$"Your new plan allows for a maximum of ({newPlan.MaxSubvaults.Value}) users. Remove some subvaults.");
|
||||
}
|
||||
}
|
||||
|
||||
var subscriptionService = new StripeSubscriptionService();
|
||||
if(string.IsNullOrWhiteSpace(organization.StripeSubscriptionId))
|
||||
{
|
||||
// They must have been on a free plan. Create new sub.
|
||||
var subCreateOptions = new StripeSubscriptionCreateOptions
|
||||
{
|
||||
Items = new List<StripeSubscriptionItemOption>
|
||||
{
|
||||
new StripeSubscriptionItemOption
|
||||
{
|
||||
PlanId = newPlan.StripePlanId,
|
||||
Quantity = 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(model.AdditionalUsers > 0)
|
||||
{
|
||||
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
||||
{
|
||||
PlanId = newPlan.StripeUserPlanId,
|
||||
Quantity = model.AdditionalUsers
|
||||
});
|
||||
}
|
||||
|
||||
await subscriptionService.CreateAsync(organization.StripeCustomerId, subCreateOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing sub.
|
||||
var subUpdateOptions = new StripeSubscriptionUpdateOptions
|
||||
{
|
||||
Items = new List<StripeSubscriptionItemUpdateOption>
|
||||
{
|
||||
new StripeSubscriptionItemUpdateOption
|
||||
{
|
||||
PlanId = newPlan.StripePlanId,
|
||||
Quantity = 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(model.AdditionalUsers > 0)
|
||||
{
|
||||
subUpdateOptions.Items.Add(new StripeSubscriptionItemUpdateOption
|
||||
{
|
||||
PlanId = newPlan.StripeUserPlanId,
|
||||
Quantity = model.AdditionalUsers
|
||||
});
|
||||
}
|
||||
|
||||
await subscriptionService.UpdateAsync(organization.StripeSubscriptionId, subUpdateOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AdjustAdditionalUsersAsync(Guid organizationId, short additionalUsers)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
if(organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(organization.StripeCustomerId))
|
||||
{
|
||||
throw new BadRequestException("No payment method found.");
|
||||
}
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(organization.StripeSubscriptionId))
|
||||
{
|
||||
throw new BadRequestException("No subscription found.");
|
||||
}
|
||||
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
if(plan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
}
|
||||
|
||||
if(!plan.CanBuyAdditionalUsers)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional users.");
|
||||
}
|
||||
|
||||
if(plan.MaxAdditionalUsers.HasValue && additionalUsers > plan.MaxAdditionalUsers.Value)
|
||||
{
|
||||
throw new BadRequestException($"Organization plan allows a maximum of " +
|
||||
$"{plan.MaxAdditionalUsers.Value} additional users.");
|
||||
}
|
||||
|
||||
var planNewMaxUsers = (short)(plan.BaseUsers + additionalUsers);
|
||||
if(!organization.MaxUsers.HasValue || organization.MaxUsers.Value > planNewMaxUsers)
|
||||
{
|
||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
||||
if(userCount >= planNewMaxUsers)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {userCount} users. Your new plan " +
|
||||
$"allows for a maximum of ({planNewMaxUsers}) users. Remove some users.");
|
||||
}
|
||||
}
|
||||
|
||||
var subscriptionService = new StripeSubscriptionService();
|
||||
var subUpdateOptions = new StripeSubscriptionUpdateOptions
|
||||
{
|
||||
Items = new List<StripeSubscriptionItemUpdateOption>
|
||||
{
|
||||
new StripeSubscriptionItemUpdateOption
|
||||
{
|
||||
PlanId = plan.StripePlanId,
|
||||
Quantity = 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(additionalUsers > 0)
|
||||
{
|
||||
subUpdateOptions.Items.Add(new StripeSubscriptionItemUpdateOption
|
||||
{
|
||||
PlanId = plan.StripeUserPlanId,
|
||||
Quantity = additionalUsers
|
||||
});
|
||||
}
|
||||
|
||||
await subscriptionService.UpdateAsync(organization.StripeSubscriptionId, subUpdateOptions);
|
||||
}
|
||||
|
||||
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup)
|
||||
{
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan && !p.Disabled);
|
||||
@ -173,6 +368,11 @@ namespace Bit.Core.Services
|
||||
StripeCustomer customer = null;
|
||||
StripeSubscription subscription = null;
|
||||
|
||||
if(!plan.CanBuyAdditionalUsers && signup.AdditionalUsers > 0)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional users.");
|
||||
}
|
||||
|
||||
if(plan.CanBuyAdditionalUsers && plan.MaxAdditionalUsers.HasValue &&
|
||||
signup.AdditionalUsers > plan.MaxAdditionalUsers.Value)
|
||||
{
|
||||
@ -204,17 +404,17 @@ namespace Bit.Core.Services
|
||||
{
|
||||
new StripeSubscriptionItemOption
|
||||
{
|
||||
PlanId = plan.CanMonthly && signup.Monthly ? plan.StripeMonthlyPlanId : plan.StripeAnnualPlanId,
|
||||
PlanId = plan.StripePlanId,
|
||||
Quantity = 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(plan.CanBuyAdditionalUsers && signup.AdditionalUsers > 0)
|
||||
if(signup.AdditionalUsers > 0)
|
||||
{
|
||||
subCreateOptions.Items.Add(new StripeSubscriptionItemOption
|
||||
{
|
||||
PlanId = plan.CanMonthly && signup.Monthly ? plan.StripeMonthlyUserPlanId : plan.StripeAnnualUserPlanId,
|
||||
PlanId = plan.StripeUserPlanId,
|
||||
Quantity = signup.AdditionalUsers
|
||||
});
|
||||
}
|
||||
@ -228,7 +428,7 @@ namespace Bit.Core.Services
|
||||
BillingEmail = signup.BillingEmail,
|
||||
BusinessName = signup.BusinessName,
|
||||
PlanType = plan.Type,
|
||||
MaxUsers = (short)(plan.BaseUsers + (plan.CanBuyAdditionalUsers ? signup.AdditionalUsers : 0)),
|
||||
MaxUsers = (short)(plan.BaseUsers + signup.AdditionalUsers),
|
||||
MaxSubvaults = plan.MaxSubvaults,
|
||||
Plan = plan.Name,
|
||||
StripeCustomerId = customer?.Id,
|
||||
|
@ -97,36 +97,45 @@ namespace Bit.Core.Utilities
|
||||
BaseUsers = 2,
|
||||
CanBuyAdditionalUsers = false,
|
||||
MaxSubvaults = 2,
|
||||
Name = "Free"
|
||||
Name = "Free",
|
||||
UpgradeSortOrder = -1 // Always the lowest plan, cannot be upgraded to
|
||||
},
|
||||
new Plan
|
||||
{
|
||||
Type = PlanType.Personal,
|
||||
Type = PlanType.PersonalAnnually,
|
||||
BaseUsers = 5,
|
||||
BaseAnnualPrice = 12,
|
||||
UserAnnualPrice = 12,
|
||||
BasePrice = 12,
|
||||
UserPrice = 12,
|
||||
CanBuyAdditionalUsers = true,
|
||||
MaxAdditionalUsers = 5,
|
||||
CanMonthly = false,
|
||||
Name = "Personal",
|
||||
StripeAnnualPlanId = "personal-annual",
|
||||
StripeAnnualUserPlanId = "personal-user-annual"
|
||||
StripePlanId = "personal-annual",
|
||||
StripeUserPlanId = "personal-user-annual",
|
||||
UpgradeSortOrder = 1
|
||||
},
|
||||
new Plan
|
||||
{
|
||||
Type = PlanType.Teams,
|
||||
Type = PlanType.TeamsMonthly,
|
||||
BaseUsers = 5,
|
||||
BaseAnnualPrice = 60,
|
||||
UserAnnualPrice = 24,
|
||||
BaseMonthlyPrice = 8,
|
||||
UserMonthlyPrice = 2.5M,
|
||||
BasePrice = 8,
|
||||
UserPrice = 2.5M,
|
||||
CanBuyAdditionalUsers = true,
|
||||
CanMonthly = true,
|
||||
Name = "Teams",
|
||||
StripeAnnualPlanId = "teams-annual",
|
||||
StripeAnnualUserPlanId = "teams-user-annual",
|
||||
StripeMonthlyPlanId = "teams-monthly",
|
||||
StripeMonthlyUserPlanId = "teams-user-monthly"
|
||||
Name = "Teams (Monthly)",
|
||||
StripePlanId = "teams-monthly",
|
||||
StripeUserPlanId = "teams-user-monthly",
|
||||
UpgradeSortOrder = 2
|
||||
},
|
||||
new Plan
|
||||
{
|
||||
Type = PlanType.TeamsAnnually,
|
||||
BaseUsers = 5,
|
||||
BasePrice = 60,
|
||||
UserPrice = 24,
|
||||
CanBuyAdditionalUsers = true,
|
||||
Name = "Teams (Annually)",
|
||||
StripePlanId = "teams-annual",
|
||||
StripeUserPlanId = "teams-user-annual",
|
||||
UpgradeSortOrder = 2
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user