diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs index 870d8dc5b8..cb0c7d6ef8 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs @@ -1,16 +1,28 @@ using Bit.Core.Models.Table; using Bit.Core.Enums; using Bit.Core.Models.Business; -using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; namespace Bit.Core.Models.Api { - public class OrganizationCreateRequestModel + public class OrganizationCreateRequestModel : IValidatableObject { + [Required] + [StringLength(50)] public string Name { get; set; } + [StringLength(50)] + public string BusinessName { get; set; } + [Required] + [StringLength(50)] + public string BillingEmail { get; set; } public PlanType PlanType { get; set; } + [Required] public string Key { get; set; } public string CardToken { get; set; } + [Range(0, double.MaxValue)] + public short AdditionalUsers { get; set; } + public bool Monthly { get; set; } public virtual OrganizationSignup ToOrganizationSignup(User user) { @@ -20,8 +32,20 @@ namespace Bit.Core.Models.Api OwnerKey = Key, Name = Name, Plan = PlanType, - PaymentToken = CardToken + PaymentToken = CardToken, + AdditionalUsers = AdditionalUsers, + BillingEmail = BillingEmail, + BusinessName = BusinessName, + Monthly = Monthly }; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if(PlanType != PlanType.Free && string.IsNullOrWhiteSpace(CardToken)) + { + yield return new ValidationResult("Card required.", new string[] { nameof(CardToken) }); + } + } } } diff --git a/src/Core/Models/Business/OrganizationSignup.cs b/src/Core/Models/Business/OrganizationSignup.cs index 340b561f4f..1842bf1a7b 100644 --- a/src/Core/Models/Business/OrganizationSignup.cs +++ b/src/Core/Models/Business/OrganizationSignup.cs @@ -6,9 +6,13 @@ namespace Bit.Core.Models.Business public class OrganizationSignup { public string Name { get; set; } + public string BusinessName { get; set; } + public string BillingEmail { get; set; } public User Owner { get; set; } public string OwnerKey { get; set; } public Enums.PlanType Plan { get; set; } + public short AdditionalUsers { get; set; } public string PaymentToken { get; set; } + public bool Monthly { get; set; } } } diff --git a/src/Core/Models/StaticStore/Plan.cs b/src/Core/Models/StaticStore/Plan.cs index 3663eb9f39..5b8386845b 100644 --- a/src/Core/Models/StaticStore/Plan.cs +++ b/src/Core/Models/StaticStore/Plan.cs @@ -6,10 +6,18 @@ namespace Bit.Core.Models.StaticStore public class Plan { public string Name { get; set; } - public string StripeId { get; set; } + public string StripeAnnualPlanId { get; set; } + public string StripeAnnualUserPlanId { get; set; } + public string StripeMonthlyPlanId { get; set; } + public string StripeMonthlyUserPlanId { get; set; } public PlanType Type { get; set; } - public short MaxUsers { get; set; } - public decimal Price { get; set; } + public short BaseUsers { get; set; } + public bool CanBuyAdditionalUsers { 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 TimeSpan? Trial { get; set; } public Func Cycle { get; set; } public bool Disabled { get; set; } diff --git a/src/Core/Models/Table/Organization.cs b/src/Core/Models/Table/Organization.cs index 207d8f342d..d9063888a4 100644 --- a/src/Core/Models/Table/Organization.cs +++ b/src/Core/Models/Table/Organization.cs @@ -9,13 +9,19 @@ namespace Bit.Core.Models.Table public Guid Id { get; set; } public Guid UserId { get; set; } public string Name { get; set; } + public string BusinessName { get; set; } + public string BillingEmail { get; set; } public string Plan { get; set; } public PlanType PlanType { get; set; } - public decimal PlanPrice { get; set; } - public decimal PlanRenewalPrice { get; set; } + public decimal PlanBasePrice { get; set; } + public decimal PlanUserPrice { get; set; } public DateTime? PlanRenewalDate { get; set; } public bool PlanTrial { get; set; } + public short BaseUsers { get; set; } + public short AdditionalUsers { get; set; } public short MaxUsers { get; set; } + public string StripeCustomerId { get; set; } + public string StripeSubscriptionId { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 134630db06..5181f1b909 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -9,6 +9,7 @@ using Bit.Core.Exceptions; using System.Collections.Generic; using Microsoft.AspNetCore.DataProtection; using Stripe; +using Bit.Core.Models.StaticStore; namespace Bit.Core.Services { @@ -49,41 +50,68 @@ namespace Bit.Core.Services } var customerService = new StripeCustomerService(); - var customer = await customerService.CreateAsync(new StripeCustomerCreateOptions - { - SourceToken = signup.PaymentToken - }); - var subscriptionService = new StripeSubscriptionService(); - var subscription = await subscriptionService.CreateAsync(customer.Id, plan.StripeId); + StripeCustomer customer = null; + StripeSubscription subscription = null; + + if(plan.Type != Enums.PlanType.Free) + { + customer = await customerService.CreateAsync(new StripeCustomerCreateOptions + { + Description = signup.BusinessName, + Email = signup.BillingEmail, + SourceToken = signup.PaymentToken + }); + + var subCreateOptions = new StripeSubscriptionCreateOptions + { + Items = new List + { + new StripeSubscriptionItemOption + { + PlanId = plan.CanMonthly && signup.Monthly ? plan.StripeMonthlyPlanId : plan.StripeAnnualPlanId, + Quantity = 1 + } + } + }; + + if(plan.CanBuyAdditionalUsers && signup.AdditionalUsers > 0) + { + subCreateOptions.Items.Add(new StripeSubscriptionItemOption + { + PlanId = plan.CanMonthly && signup.Monthly ? plan.StripeMonthlyUserPlanId : plan.StripeAnnualUserPlanId, + Quantity = signup.AdditionalUsers + }); + } + + subscription = await subscriptionService.CreateAsync(customer.Id, subCreateOptions); + } var organization = new Organization { Name = signup.Name, + BillingEmail = signup.BillingEmail, + BusinessName = signup.BusinessName, UserId = signup.Owner.Id, PlanType = plan.Type, - MaxUsers = plan.MaxUsers, + BaseUsers = plan.BaseUsers, + AdditionalUsers = (short)(plan.CanBuyAdditionalUsers ? signup.AdditionalUsers : 0), + MaxUsers = (short)(plan.BaseUsers + (plan.CanBuyAdditionalUsers ? signup.AdditionalUsers : 0)), PlanTrial = plan.Trial.HasValue, - PlanPrice = plan.Trial.HasValue ? 0 : plan.Price, - PlanRenewalPrice = plan.Price, + PlanBasePrice = plan.CanMonthly && signup.Monthly ? plan.BaseMonthlyPrice : plan.BaseAnnualPrice, + PlanUserPrice = plan.CanMonthly && signup.Monthly ? plan.UserMonthlyPrice : plan.UserAnnualPrice, + PlanRenewalDate = subscription?.CurrentPeriodEnd, Plan = plan.ToString(), + StripeCustomerId = customer?.Id, + StripeSubscriptionId = subscription?.Id, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow }; - if(plan.Trial.HasValue) - { - organization.PlanRenewalDate = DateTime.UtcNow.Add(plan.Trial.Value); - } - else if(plan.Cycle != null) - { - organization.PlanRenewalDate = DateTime.UtcNow.Add(plan.Cycle(DateTime.UtcNow)); - } - - await _organizationRepository.CreateAsync(organization); - try { + await _organizationRepository.CreateAsync(organization); + var orgUser = new OrganizationUser { OrganizationId = organization.Id, @@ -102,7 +130,18 @@ namespace Bit.Core.Services } catch { - await _organizationRepository.DeleteAsync(organization); + if(subscription != null) + { + await subscriptionService.CancelAsync(subscription.Id); + } + + // TODO: reverse payments + + if(organization.Id != default(Guid)) + { + await _organizationRepository.DeleteAsync(organization); + } + throw; } } diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index a70b2dc8d0..192c4b144f 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -94,27 +94,40 @@ namespace Bit.Core.Utilities new Plan { Type = PlanType.Free, - MaxUsers = 1, - Price = 0 + BaseUsers = 1, + CanBuyAdditionalUsers = false, + Name = "Free" }, new Plan { Type = PlanType.Personal, - MaxUsers = 5, - Price = 1, + BaseUsers = 5, + BaseAnnualPrice = 12, + UserAnnualPrice = 12, + CanBuyAdditionalUsers = true, Trial = new TimeSpan(14, 0, 0, 0), Cycle = now => now.AddYears(1) - now, Name = "Personal", - StripeId = "premium-yearly" - + StripeAnnualPlanId = "premium-yearly", + StripeAnnualUserPlanId = "premium-user-yearly" }, new Plan { Type = PlanType.Teams, - MaxUsers = 5, - Price = 10, + BaseUsers = 5, + BaseAnnualPrice = 60, + UserAnnualPrice = 24, + BaseMonthlyPrice = 8, + UserMonthlyPrice = 2.5M, + CanBuyAdditionalUsers = true, + CanMonthly = true, Trial = new TimeSpan(14, 0, 0, 0), - Cycle = now => now.AddMonths(1) - now + Cycle = now => now.AddMonths(1) - now, + Name = "Teams", + StripeAnnualPlanId = "premium-yearly", + StripeAnnualUserPlanId = "premium-user-yearly", + StripeMonthlyPlanId = "premium-yearly", + StripeMonthlyUserPlanId = "premium-user-yearly" } }; diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index acdb437aa9..3fbe6894a6 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -67,24 +67,26 @@ - + + + - - - - - - + + + + + + + - @@ -92,14 +94,16 @@ + + + - @@ -157,10 +161,19 @@ + + + + + + + + + @@ -168,19 +181,9 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/Sql/dbo/Functions/UserCanEditCipher.sql b/src/Sql/dbo/Functions/UserCanEditCipher.sql index 4405ed7d44..f847957729 100644 --- a/src/Sql/dbo/Functions/UserCanEditCipher.sql +++ b/src/Sql/dbo/Functions/UserCanEditCipher.sql @@ -31,4 +31,4 @@ BEGIN [CanEdit] = 1 RETURN @CanEdit -END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByRevisionDateUserWithDeleteHistory.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByRevisionDateUserWithDeleteHistory.sql index e50e40b2e1..724cdeb4a1 100644 --- a/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByRevisionDateUserWithDeleteHistory.sql +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_ReadByRevisionDateUserWithDeleteHistory.sql @@ -21,4 +21,4 @@ BEGIN [Date] > @SinceRevisionDate AND [Event] = 2 -- Only cipher delete events. AND [UserId] = @UserId -END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql index 844cce38e5..ef10df8040 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql @@ -2,13 +2,19 @@ @Id UNIQUEIDENTIFIER, @UserId UNIQUEIDENTIFIER, @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BillingEmail NVARCHAR(50), @Plan NVARCHAR(20), @PlanType TINYINT, - @PlanPrice MONEY, - @PlanRenewalPrice MONEY, + @PlanBasePrice MONEY, + @PlanUserPrice MONEY, @PlanRenewalDate DATETIME2(7), @PlanTrial BIT, + @BaseUsers SMALLINT, + @AdditionalUsers SMALLINT, @MaxUsers SMALLINT, + @StripeCustomerId VARCHAR(50), + @StripeSubscriptionId VARCHAR(50), @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7) AS @@ -20,13 +26,19 @@ BEGIN [Id], [UserId], [Name], + [BusinessName], + [BillingEmail], [Plan], [PlanType], - [PlanPrice], - [PlanRenewalPrice], + [PlanBasePrice], + [PlanUserPrice], [PlanRenewalDate], [PlanTrial], + [BaseUsers], + [AdditionalUsers], [MaxUsers], + [StripeCustomerId], + [StripeSubscriptionId], [CreationDate], [RevisionDate] ) @@ -35,13 +47,19 @@ BEGIN @Id, @UserId, @Name, + @BusinessName, + @BillingEmail, @Plan, @PlanType, - @PlanPrice, - @PlanRenewalPrice, + @PlanBasePrice, + @PlanUserPrice, @PlanRenewalDate, @PlanTrial, + @BaseUsers, + @AdditionalUsers, @MaxUsers, + @StripeCustomerId, + @StripeSubscriptionId, @CreationDate, @RevisionDate ) diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql index 5ab985c967..ba3ba85d88 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql @@ -2,15 +2,22 @@ @Id UNIQUEIDENTIFIER, @UserId UNIQUEIDENTIFIER, @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BillingEmail NVARCHAR(50), @Plan NVARCHAR(20), @PlanType TINYINT, - @PlanPrice MONEY, - @PlanRenewalPrice MONEY, + @PlanBasePrice MONEY, + @PlanUserPrice MONEY, @PlanRenewalDate DATETIME2(7), @PlanTrial BIT, + @BaseUsers SMALLINT, + @AdditionalUsers SMALLINT, @MaxUsers SMALLINT, + @StripeCustomerId VARCHAR(50), + @StripeSubscriptionId VARCHAR(50), @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7) + AS BEGIN SET NOCOUNT ON @@ -20,13 +27,19 @@ BEGIN SET [UserId] = @UserId, [Name] = @Name, + [BusinessName] = @BusinessName, + [BillingEmail] = @BillingEmail, [Plan] = @Plan, [PlanType] = @PlanType, - [PlanPrice] = @PlanPrice, - [PlanRenewalPrice] = @PlanRenewalPrice, + [PlanBasePrice] = @PlanBasePrice, + [PlanUserPrice] = @PlanUserPrice, [PlanRenewalDate] = @PlanRenewalDate, [PlanTrial] = @PlanTrial, + [BaseUsers] = @BaseUsers, + [AdditionalUsers] = @AdditionalUsers, [MaxUsers] = @MaxUsers, + [StripeCustomerId] = @StripeCustomerId, + [StripeSubscriptionId] = @StripeSubscriptionId, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate WHERE diff --git a/src/Sql/dbo/Stored Procedures/SubvaultCipher_ReadByUserId.sql b/src/Sql/dbo/Stored Procedures/SubvaultCipher_ReadByUserId.sql index db2210671a..990b974003 100644 --- a/src/Sql/dbo/Stored Procedures/SubvaultCipher_ReadByUserId.sql +++ b/src/Sql/dbo/Stored Procedures/SubvaultCipher_ReadByUserId.sql @@ -15,4 +15,4 @@ BEGIN WHERE OU.[UserId] = @UserId AND OU.[Status] = 2 -- Confirmed -END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadCanEditByCipherIdUserId.sql b/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadCanEditByCipherIdUserId.sql index 925d779630..7143d13ce1 100644 --- a/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadCanEditByCipherIdUserId.sql +++ b/src/Sql/dbo/Stored Procedures/SubvaultUser_ReadCanEditByCipherIdUserId.sql @@ -7,4 +7,4 @@ BEGIN SELECT [dbo].[UserCanEditCipher](@UserId, @CipherId) -END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Subvault_ReadByUserId.sql b/src/Sql/dbo/Stored Procedures/Subvault_ReadByUserId.sql index 2811a41229..7edccd32eb 100644 --- a/src/Sql/dbo/Stored Procedures/Subvault_ReadByUserId.sql +++ b/src/Sql/dbo/Stored Procedures/Subvault_ReadByUserId.sql @@ -15,4 +15,4 @@ BEGIN WHERE OU.[UserId] = @UserId AND OU.[Status] = 2 -- Confirmed -END +END \ No newline at end of file diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql index 03544dac6a..bae5de3605 100644 --- a/src/Sql/dbo/Tables/Organization.sql +++ b/src/Sql/dbo/Tables/Organization.sql @@ -1,16 +1,22 @@ CREATE TABLE [dbo].[Organization] ( - [Id] UNIQUEIDENTIFIER NOT NULL, - [UserId] UNIQUEIDENTIFIER NOT NULL, - [Name] NVARCHAR (50) NOT NULL, - [Plan] NVARCHAR (20) NOT NULL, - [PlanType] TINYINT NOT NULL, - [PlanPrice] MONEY NOT NULL, - [PlanRenewalPrice] MONEY NOT NULL, - [PlanRenewalDate] DATETIME2 (7) NULL, - [PlanTrial] BIT NOT NULL, - [MaxUsers] SMALLINT NULL, - [CreationDate] DATETIME2 (7) NOT NULL, - [RevisionDate] DATETIME2 (7) NOT NULL, + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NOT NULL, + [Name] NVARCHAR (50) NOT NULL, + [BusinessName] NVARCHAR (50) NULL, + [BillingEmail] NVARCHAR (50) NOT NULL, + [Plan] NVARCHAR (20) NOT NULL, + [PlanType] TINYINT NOT NULL, + [PlanBasePrice] MONEY NOT NULL, + [PlanUserPrice] MONEY NOT NULL, + [PlanRenewalDate] DATETIME2 (7) NULL, + [PlanTrial] BIT NOT NULL, + [BaseUsers] SMALLINT NULL, + [AdditionalUsers] SMALLINT NULL, + [MaxUsers] SMALLINT NULL, + [StripeCustomerId] VARCHAR (50) NULL, + [StripeSubscriptionId] VARCHAR (50) NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_Organization_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) );