1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-22 12:04:27 -05:00

Billing updates

- Break monthly and annual plans into two.
- Add upgrade and adjust additional users
This commit is contained in:
Kyle Spearrin 2017-04-10 09:36:21 -04:00
parent 52dcd6d6ab
commit bb0555a6d9
5 changed files with 255 additions and 36 deletions

View File

@ -3,9 +3,11 @@
public enum PlanType : byte public enum PlanType : byte
{ {
Free = 0, Free = 0,
Personal = 1, PersonalAnnually = 1,
Teams = 2, TeamsMonthly = 2,
Enterprise = 3, TeamsAnnually = 3,
Custom = 4 EnterpriseMonthly = 4,
EnterpriseAnnually = 5,
Custom = 6
} }
} }

View 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; }
}
}

View File

@ -1,25 +1,20 @@
using Bit.Core.Enums; using Bit.Core.Enums;
using System;
namespace Bit.Core.Models.StaticStore namespace Bit.Core.Models.StaticStore
{ {
public class Plan public class Plan
{ {
public string Name { get; set; } public string Name { get; set; }
public string StripeAnnualPlanId { get; set; } public string StripePlanId { get; set; }
public string StripeAnnualUserPlanId { get; set; } public string StripeUserPlanId { get; set; }
public string StripeMonthlyPlanId { get; set; }
public string StripeMonthlyUserPlanId { get; set; }
public PlanType Type { get; set; } public PlanType Type { get; set; }
public short BaseUsers { get; set; } public short BaseUsers { get; set; }
public bool CanBuyAdditionalUsers { get; set; } public bool CanBuyAdditionalUsers { get; set; }
public short? MaxAdditionalUsers { get; set; } public short? MaxAdditionalUsers { get; set; }
public bool CanMonthly { get; set; } public decimal BasePrice { get; set; }
public decimal BaseMonthlyPrice { get; set; } public decimal UserPrice { get; set; }
public decimal UserMonthlyPrice { get; set; }
public decimal BaseAnnualPrice { get; set; }
public decimal UserAnnualPrice { get; set; }
public short? MaxSubvaults { get; set; } public short? MaxSubvaults { get; set; }
public int UpgradeSortOrder { get; set; }
public bool Disabled { get; set; } public bool Disabled { get; set; }
} }
} }

View File

@ -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) public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup)
{ {
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan && !p.Disabled); var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan && !p.Disabled);
@ -173,6 +368,11 @@ namespace Bit.Core.Services
StripeCustomer customer = null; StripeCustomer customer = null;
StripeSubscription subscription = 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 && if(plan.CanBuyAdditionalUsers && plan.MaxAdditionalUsers.HasValue &&
signup.AdditionalUsers > plan.MaxAdditionalUsers.Value) signup.AdditionalUsers > plan.MaxAdditionalUsers.Value)
{ {
@ -204,17 +404,17 @@ namespace Bit.Core.Services
{ {
new StripeSubscriptionItemOption new StripeSubscriptionItemOption
{ {
PlanId = plan.CanMonthly && signup.Monthly ? plan.StripeMonthlyPlanId : plan.StripeAnnualPlanId, PlanId = plan.StripePlanId,
Quantity = 1 Quantity = 1
} }
} }
}; };
if(plan.CanBuyAdditionalUsers && signup.AdditionalUsers > 0) if(signup.AdditionalUsers > 0)
{ {
subCreateOptions.Items.Add(new StripeSubscriptionItemOption subCreateOptions.Items.Add(new StripeSubscriptionItemOption
{ {
PlanId = plan.CanMonthly && signup.Monthly ? plan.StripeMonthlyUserPlanId : plan.StripeAnnualUserPlanId, PlanId = plan.StripeUserPlanId,
Quantity = signup.AdditionalUsers Quantity = signup.AdditionalUsers
}); });
} }
@ -228,7 +428,7 @@ namespace Bit.Core.Services
BillingEmail = signup.BillingEmail, BillingEmail = signup.BillingEmail,
BusinessName = signup.BusinessName, BusinessName = signup.BusinessName,
PlanType = plan.Type, PlanType = plan.Type,
MaxUsers = (short)(plan.BaseUsers + (plan.CanBuyAdditionalUsers ? signup.AdditionalUsers : 0)), MaxUsers = (short)(plan.BaseUsers + signup.AdditionalUsers),
MaxSubvaults = plan.MaxSubvaults, MaxSubvaults = plan.MaxSubvaults,
Plan = plan.Name, Plan = plan.Name,
StripeCustomerId = customer?.Id, StripeCustomerId = customer?.Id,

View File

@ -97,36 +97,45 @@ namespace Bit.Core.Utilities
BaseUsers = 2, BaseUsers = 2,
CanBuyAdditionalUsers = false, CanBuyAdditionalUsers = false,
MaxSubvaults = 2, MaxSubvaults = 2,
Name = "Free" Name = "Free",
UpgradeSortOrder = -1 // Always the lowest plan, cannot be upgraded to
}, },
new Plan new Plan
{ {
Type = PlanType.Personal, Type = PlanType.PersonalAnnually,
BaseUsers = 5, BaseUsers = 5,
BaseAnnualPrice = 12, BasePrice = 12,
UserAnnualPrice = 12, UserPrice = 12,
CanBuyAdditionalUsers = true, CanBuyAdditionalUsers = true,
MaxAdditionalUsers = 5, MaxAdditionalUsers = 5,
CanMonthly = false,
Name = "Personal", Name = "Personal",
StripeAnnualPlanId = "personal-annual", StripePlanId = "personal-annual",
StripeAnnualUserPlanId = "personal-user-annual" StripeUserPlanId = "personal-user-annual",
UpgradeSortOrder = 1
}, },
new Plan new Plan
{ {
Type = PlanType.Teams, Type = PlanType.TeamsMonthly,
BaseUsers = 5, BaseUsers = 5,
BaseAnnualPrice = 60, BasePrice = 8,
UserAnnualPrice = 24, UserPrice = 2.5M,
BaseMonthlyPrice = 8,
UserMonthlyPrice = 2.5M,
CanBuyAdditionalUsers = true, CanBuyAdditionalUsers = true,
CanMonthly = true, Name = "Teams (Monthly)",
Name = "Teams", StripePlanId = "teams-monthly",
StripeAnnualPlanId = "teams-annual", StripeUserPlanId = "teams-user-monthly",
StripeAnnualUserPlanId = "teams-user-annual", UpgradeSortOrder = 2
StripeMonthlyPlanId = "teams-monthly", },
StripeMonthlyUserPlanId = "teams-user-monthly" 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
} }
}; };