1
0
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:
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
{
Free = 0,
Personal = 1,
Teams = 2,
Enterprise = 3,
Custom = 4
PersonalAnnually = 1,
TeamsMonthly = 2,
TeamsAnnually = 3,
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 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; }
}
}

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)
{
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,

View File

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