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:
parent
52dcd6d6ab
commit
bb0555a6d9
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user