mirror of
https://github.com/bitwarden/server
synced 2025-12-19 09:43:25 +00:00
[AC 1409] Secrets Manager Subscription Stripe Integration (#3019)
* Adding the Secret manager to the Plan List * Adding the unit test for the StaticStoreTests class * Fix whitespace formatting * Fix whitespace formatting * Price update * Resolving the PR comments * Resolving PR comments * Fixing the whitespace * only password manager plans are return for now * format whitespace * Resolve the test issue * Fixing the failing test * Refactoring the Plan separation * add a unit test for SingleOrDefault * Fix the whitespace format * Separate the PM and SM plans * Fixing the whitespace * Remove unnecessary directive * Fix imports ordering * Fix imports ordering * Resolve imports ordering * Fixing imports ordering * Fix response model, add MaxProjects * Fix filename * Fix format * Fix: seat price should match annual/monthly * Fix service account annual pricing * Changes for secret manager signup and upgradeplan * Changes for secrets manager signup and upgrade * refactoring the code * Format whitespace * remove unnecessary using directive * Resolve the PR comment on Subscription creation * Resolve PR comment * Add password manager to the error message * Add UseSecretsManager to the event log * Resolve PR comment on plan validation * Resolving pr comments for service account count * Resolving pr comments for service account count * Resolve the pr comments * Remove the store procedure that is no-longer needed * Rename a property properly * Resolving the PR comment * Resolve PR comments * Resolving PR comments * Resolving the Pr comments * Resolving some PR comments * Resolving the PR comments * Resolving the build identity build * Add additional Validation * Resolve the Lint issues * remove unnecessary using directive * Remove the white spaces * Adding unit test for the stripe payment * Remove the incomplete test * Fixing the failing test * Fix the failing test * Fix the fail test on organization service * Fix the failing unit test * Fix the whitespace format * Fix the failing test * Fix the whitespace format * resolve pr comments * Fix the lint message * Resolve the PR comments * resolve pr comments * Resolve pr comments * Resolve the pr comments * remove unused code * Added for sm validation test * Fix the whitespace format issues --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
@@ -130,6 +130,16 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
|
|||||||
return policy == null ? (false, false) : (policy.Read, policy.Write);
|
return policy == null ? (false, false) : (policy.Read, policy.Write);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
return await dbContext.ServiceAccount
|
||||||
|
.CountAsync(ou => ou.OrganizationId == organizationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
|
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
|
||||||
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||||
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
|
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ public class Startup
|
|||||||
// Repositories
|
// Repositories
|
||||||
services.AddDatabaseRepositories(globalSettings);
|
services.AddDatabaseRepositories(globalSettings);
|
||||||
|
|
||||||
|
services.AddOosServices();
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
services.AddScoped<ICurrentContext, CurrentContext>();
|
services.AddScoped<ICurrentContext, CurrentContext>();
|
||||||
services.AddScoped<IScimContext, ScimContext>();
|
services.AddScoped<IScimContext, ScimContext>();
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
|||||||
[StringLength(2)]
|
[StringLength(2)]
|
||||||
public string BillingAddressCountry { get; set; }
|
public string BillingAddressCountry { get; set; }
|
||||||
public int? MaxAutoscaleSeats { get; set; }
|
public int? MaxAutoscaleSeats { get; set; }
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int? AdditionalSmSeats { get; set; }
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int? AdditionalServiceAccounts { get; set; }
|
||||||
|
[Required]
|
||||||
|
public bool UseSecretsManager { get; set; }
|
||||||
|
|
||||||
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
||||||
{
|
{
|
||||||
@@ -58,6 +64,9 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
|||||||
BillingEmail = BillingEmail,
|
BillingEmail = BillingEmail,
|
||||||
BusinessName = BusinessName,
|
BusinessName = BusinessName,
|
||||||
CollectionName = CollectionName,
|
CollectionName = CollectionName,
|
||||||
|
AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(),
|
||||||
|
AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(),
|
||||||
|
UseSecretsManager = UseSecretsManager,
|
||||||
TaxInfo = new TaxInfo
|
TaxInfo = new TaxInfo
|
||||||
{
|
{
|
||||||
TaxIdNumber = TaxIdNumber,
|
TaxIdNumber = TaxIdNumber,
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ public class OrganizationUpgradeRequestModel
|
|||||||
public int AdditionalSeats { get; set; }
|
public int AdditionalSeats { get; set; }
|
||||||
[Range(0, 99)]
|
[Range(0, 99)]
|
||||||
public short? AdditionalStorageGb { get; set; }
|
public short? AdditionalStorageGb { get; set; }
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int? AdditionalSmSeats { get; set; }
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int? AdditionalServiceAccounts { get; set; }
|
||||||
|
[Required]
|
||||||
|
public bool UseSecretsManager { get; set; }
|
||||||
public bool PremiumAccessAddon { get; set; }
|
public bool PremiumAccessAddon { get; set; }
|
||||||
public string BillingAddressCountry { get; set; }
|
public string BillingAddressCountry { get; set; }
|
||||||
public string BillingAddressPostalCode { get; set; }
|
public string BillingAddressPostalCode { get; set; }
|
||||||
@@ -24,6 +30,9 @@ public class OrganizationUpgradeRequestModel
|
|||||||
{
|
{
|
||||||
AdditionalSeats = AdditionalSeats,
|
AdditionalSeats = AdditionalSeats,
|
||||||
AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(),
|
AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(),
|
||||||
|
AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(0),
|
||||||
|
AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(0),
|
||||||
|
UseSecretsManager = UseSecretsManager,
|
||||||
BusinessName = BusinessName,
|
BusinessName = BusinessName,
|
||||||
Plan = PlanType,
|
Plan = PlanType,
|
||||||
PremiumAccessAddon = PremiumAccessAddon,
|
PremiumAccessAddon = PremiumAccessAddon,
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ public class OrganizationUpgrade
|
|||||||
public TaxInfo TaxInfo { get; set; }
|
public TaxInfo TaxInfo { get; set; }
|
||||||
public string PublicKey { get; set; }
|
public string PublicKey { get; set; }
|
||||||
public string PrivateKey { get; set; }
|
public string PrivateKey { get; set; }
|
||||||
|
public int? AdditionalSmSeats { get; set; }
|
||||||
|
public int? AdditionalServiceAccounts { get; set; }
|
||||||
|
public bool UseSecretsManager { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,61 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Business;
|
namespace Bit.Core.Models.Business;
|
||||||
|
|
||||||
public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions
|
public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions
|
||||||
{
|
{
|
||||||
public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo, int additionalSeats, int additionalStorageGb, bool premiumAccessAddon)
|
public OrganizationSubscriptionOptionsBase(Organization org, List<StaticStore.Plan> plans, TaxInfo taxInfo, int additionalSeats,
|
||||||
|
int additionalStorageGb, bool premiumAccessAddon, int additionalSmSeats = 0, int additionalServiceAccounts = 0)
|
||||||
{
|
{
|
||||||
Items = new List<SubscriptionItemOptions>();
|
Items = new List<SubscriptionItemOptions>();
|
||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
[org.GatewayIdField()] = org.Id.ToString()
|
[org.GatewayIdField()] = org.Id.ToString()
|
||||||
};
|
};
|
||||||
|
foreach (var plan in plans)
|
||||||
|
{
|
||||||
|
AddPlanIdToSubscription(plan);
|
||||||
|
|
||||||
if (plan.StripePlanId != null)
|
switch (plan.BitwardenProduct)
|
||||||
|
{
|
||||||
|
case BitwardenProductType.PasswordManager:
|
||||||
|
{
|
||||||
|
AddPremiumAccessAddon(premiumAccessAddon, plan);
|
||||||
|
AddAdditionalSeatToSubscription(additionalSeats, plan);
|
||||||
|
AddAdditionalStorage(additionalStorageGb, plan);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BitwardenProductType.SecretsManager:
|
||||||
|
{
|
||||||
|
AddAdditionalSeatToSubscription(additionalSmSeats, plan);
|
||||||
|
AddServiceAccount(additionalServiceAccounts, plan);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId))
|
||||||
|
{
|
||||||
|
DefaultTaxRates = new List<string> { taxInfo.StripeTaxRateId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddServiceAccount(int additionalServiceAccounts, StaticStore.Plan plan)
|
||||||
|
{
|
||||||
|
if (additionalServiceAccounts > 0 && plan.StripeServiceAccountPlanId != null)
|
||||||
{
|
{
|
||||||
Items.Add(new SubscriptionItemOptions
|
Items.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Plan = plan.StripePlanId,
|
Plan = plan.StripeServiceAccountPlanId,
|
||||||
Quantity = 1
|
Quantity = additionalServiceAccounts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (additionalSeats > 0 && plan.StripeSeatPlanId != null)
|
|
||||||
{
|
|
||||||
Items.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Plan = plan.StripeSeatPlanId,
|
|
||||||
Quantity = additionalSeats
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddAdditionalStorage(int additionalStorageGb, StaticStore.Plan plan)
|
||||||
|
{
|
||||||
if (additionalStorageGb > 0)
|
if (additionalStorageGb > 0)
|
||||||
{
|
{
|
||||||
Items.Add(new SubscriptionItemOptions
|
Items.Add(new SubscriptionItemOptions
|
||||||
@@ -39,19 +64,29 @@ public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOpti
|
|||||||
Quantity = additionalStorageGb
|
Quantity = additionalStorageGb
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (premiumAccessAddon && plan.StripePremiumAccessPlanId != null)
|
|
||||||
{
|
|
||||||
Items.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Plan = plan.StripePremiumAccessPlanId,
|
|
||||||
Quantity = 1
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId))
|
private void AddPremiumAccessAddon(bool premiumAccessAddon, StaticStore.Plan plan)
|
||||||
{
|
{
|
||||||
DefaultTaxRates = new List<string> { taxInfo.StripeTaxRateId };
|
if (premiumAccessAddon && plan.StripePremiumAccessPlanId != null)
|
||||||
|
{
|
||||||
|
Items.Add(new SubscriptionItemOptions { Plan = plan.StripePremiumAccessPlanId, Quantity = 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddAdditionalSeatToSubscription(int additionalSeats, StaticStore.Plan plan)
|
||||||
|
{
|
||||||
|
if (additionalSeats > 0 && plan.StripeSeatPlanId != null)
|
||||||
|
{
|
||||||
|
Items.Add(new SubscriptionItemOptions { Plan = plan.StripeSeatPlanId, Quantity = additionalSeats });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddPlanIdToSubscription(StaticStore.Plan plan)
|
||||||
|
{
|
||||||
|
if (plan.StripePlanId != null)
|
||||||
|
{
|
||||||
|
Items.Add(new SubscriptionItemOptions { Plan = plan.StripePlanId, Quantity = 1 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,13 +94,14 @@ public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOpti
|
|||||||
public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase
|
public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase
|
||||||
{
|
{
|
||||||
public OrganizationPurchaseSubscriptionOptions(
|
public OrganizationPurchaseSubscriptionOptions(
|
||||||
Organization org, StaticStore.Plan plan,
|
Organization org, List<StaticStore.Plan> plans,
|
||||||
TaxInfo taxInfo, int additionalSeats = 0,
|
TaxInfo taxInfo, int additionalSeats = 0,
|
||||||
int additionalStorageGb = 0, bool premiumAccessAddon = false) :
|
int additionalStorageGb = 0, bool premiumAccessAddon = false,
|
||||||
base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon)
|
int additionalSmSeats = 0, int additionalServiceAccounts = 0) :
|
||||||
|
base(org, plans, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon, additionalSmSeats, additionalServiceAccounts)
|
||||||
{
|
{
|
||||||
OffSession = true;
|
OffSession = true;
|
||||||
TrialPeriodDays = plan.TrialPeriodDays;
|
TrialPeriodDays = plans.FirstOrDefault(x => x.BitwardenProduct == BitwardenProductType.PasswordManager)!.TrialPeriodDays;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,10 +109,10 @@ public class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOp
|
|||||||
{
|
{
|
||||||
public OrganizationUpgradeSubscriptionOptions(
|
public OrganizationUpgradeSubscriptionOptions(
|
||||||
string customerId, Organization org,
|
string customerId, Organization org,
|
||||||
StaticStore.Plan plan, TaxInfo taxInfo,
|
List<StaticStore.Plan> plans, TaxInfo taxInfo,
|
||||||
int additionalSeats = 0, int additionalStorageGb = 0,
|
int additionalSeats = 0, int additionalStorageGb = 0,
|
||||||
bool premiumAccessAddon = false) :
|
bool premiumAccessAddon = false, int additionalSmSeats = 0, int additionalServiceAccounts = 0) :
|
||||||
base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon)
|
base(org, plans, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon, additionalSmSeats, additionalServiceAccounts)
|
||||||
{
|
{
|
||||||
Customer = customerId;
|
Customer = customerId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,4 +40,5 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
|||||||
Task RevokeAsync(Guid id);
|
Task RevokeAsync(Guid id);
|
||||||
Task RestoreAsync(Guid id, OrganizationUserStatusType status);
|
Task RestoreAsync(Guid id, OrganizationUserStatusType status);
|
||||||
Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType);
|
Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType);
|
||||||
|
Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/Core/Repositories/Noop/NoopServiceAccountRepository.cs
Normal file
60
src/Core/Repositories/Noop/NoopServiceAccountRepository.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.Repositories.Noop;
|
||||||
|
|
||||||
|
public class NoopServiceAccountRepository : IServiceAccountRepository
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||||
|
{
|
||||||
|
return Task.FromResult(null as IEnumerable<ServiceAccount>);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ServiceAccount> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
return Task.FromResult(null as ServiceAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<ServiceAccount>> GetManyByIds(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
return Task.FromResult(null as IEnumerable<ServiceAccount>);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount)
|
||||||
|
{
|
||||||
|
return Task.FromResult(null as ServiceAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ReplaceAsync(ServiceAccount serviceAccount)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> UserHasReadAccessToServiceAccount(Guid id, Guid userId)
|
||||||
|
{
|
||||||
|
return Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId)
|
||||||
|
{
|
||||||
|
return Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId, AccessClientType accessType)
|
||||||
|
{
|
||||||
|
return Task.FromResult((false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,4 +15,5 @@ public interface IServiceAccountRepository
|
|||||||
Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId);
|
Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId);
|
||||||
Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||||
Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId, AccessClientType accessType);
|
Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId, AccessClientType accessType);
|
||||||
|
Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ public interface IPaymentService
|
|||||||
{
|
{
|
||||||
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
||||||
Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||||
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
|
string paymentToken, List<Plan> plans, short additionalStorageGb, int additionalSeats,
|
||||||
bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false);
|
bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0,
|
||||||
|
int additionalServiceAccount = 0);
|
||||||
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
|
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||||
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
|
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||||
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan,
|
Task<string> UpgradeFreeOrganizationAsync(Organization org, List<Plan> plans,
|
||||||
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo);
|
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo,
|
||||||
|
int additionalSmSeats = 0, int additionalServiceAccounts = 0);
|
||||||
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
||||||
short additionalStorageGb, TaxInfo taxInfo);
|
short additionalStorageGb, TaxInfo taxInfo);
|
||||||
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Bit.Core.Models.Business;
|
|||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations.Policies;
|
using Bit.Core.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
@@ -53,6 +54,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly ILogger<OrganizationService> _logger;
|
private readonly ILogger<OrganizationService> _logger;
|
||||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||||
private readonly IProviderUserRepository _providerUserRepository;
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
|
|
||||||
public OrganizationService(
|
public OrganizationService(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@@ -81,7 +83,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ILogger<OrganizationService> logger,
|
ILogger<OrganizationService> logger,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IProviderUserRepository providerUserRepository)
|
IProviderUserRepository providerUserRepository,
|
||||||
|
IServiceAccountRepository serviceAccountRepository)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@@ -110,6 +113,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_providerOrganizationRepository = providerOrganizationRepository;
|
_providerOrganizationRepository = providerOrganizationRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
|
_serviceAccountRepository = serviceAccountRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
||||||
@@ -179,60 +183,69 @@ public class OrganizationService : IOrganizationService
|
|||||||
throw new BadRequestException("Your account has no payment method available.");
|
throw new BadRequestException("Your account has no payment method available.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
|
var existingPasswordManagerPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||||
if (existingPlan == null)
|
if (existingPasswordManagerPlan == null)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Existing plan not found.");
|
throw new BadRequestException("Existing plan not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
|
var newPasswordManagerPlan =
|
||||||
if (newPlan == null)
|
StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
|
||||||
|
if (newPasswordManagerPlan == null)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Plan not found.");
|
throw new BadRequestException("Plan not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingPlan.Type == newPlan.Type)
|
if (existingPasswordManagerPlan.Type == newPasswordManagerPlan.Type)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Organization is already on this plan.");
|
throw new BadRequestException("Organization is already on this plan.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingPlan.UpgradeSortOrder >= newPlan.UpgradeSortOrder)
|
if (existingPasswordManagerPlan.UpgradeSortOrder >= newPasswordManagerPlan.UpgradeSortOrder)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("You cannot upgrade to this plan.");
|
throw new BadRequestException("You cannot upgrade to this plan.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingPlan.Type != PlanType.Free)
|
if (existingPasswordManagerPlan.Type != PlanType.Free)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
|
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateOrganizationUpgradeParameters(newPlan, upgrade);
|
ValidatePasswordManagerPlan(newPasswordManagerPlan, upgrade);
|
||||||
|
var newSecretsManagerPlan =
|
||||||
var newPlanSeats = (short)(newPlan.BaseSeats +
|
StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
|
||||||
(newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
if (upgrade.UseSecretsManager)
|
||||||
if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
|
|
||||||
{
|
{
|
||||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
ValidateSecretsManagerPlan(newSecretsManagerPlan, upgrade);
|
||||||
if (occupiedSeats > newPlanSeats)
|
}
|
||||||
|
|
||||||
|
var newPasswordManagerPlanSeats = (short)(newPasswordManagerPlan.BaseSeats +
|
||||||
|
(newPasswordManagerPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
||||||
|
if (!organization.Seats.HasValue || organization.Seats.Value > newPasswordManagerPlanSeats)
|
||||||
|
{
|
||||||
|
var occupiedSeats =
|
||||||
|
await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
|
if (occupiedSeats > newPasswordManagerPlanSeats)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
||||||
$"Your new plan only has ({newPlanSeats}) seats. Remove some users.");
|
$"Your new plan only has ({newPasswordManagerPlanSeats}) seats. Remove some users.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
|
if (newPasswordManagerPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
|
||||||
organization.MaxCollections.Value > newPlan.MaxCollections.Value))
|
organization.MaxCollections.Value >
|
||||||
|
newPasswordManagerPlan.MaxCollections.Value))
|
||||||
{
|
{
|
||||||
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
|
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
|
||||||
if (collectionCount > newPlan.MaxCollections.Value)
|
if (collectionCount > newPasswordManagerPlan.MaxCollections.Value)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Your organization currently has {collectionCount} collections. " +
|
throw new BadRequestException($"Your organization currently has {collectionCount} collections. " +
|
||||||
$"Your new plan allows for a maximum of ({newPlan.MaxCollections.Value}) collections. " +
|
$"Your new plan allows for a maximum of ({newPasswordManagerPlan.MaxCollections.Value}) collections. " +
|
||||||
"Remove some collections.");
|
"Remove some collections.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPlan.HasGroups && organization.UseGroups)
|
if (!newPasswordManagerPlan.HasGroups && organization.UseGroups)
|
||||||
{
|
{
|
||||||
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
|
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
|
||||||
if (groups.Any())
|
if (groups.Any())
|
||||||
@@ -242,7 +255,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPlan.HasPolicies && organization.UsePolicies)
|
if (!newPasswordManagerPlan.HasPolicies && organization.UsePolicies)
|
||||||
{
|
{
|
||||||
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id);
|
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id);
|
||||||
if (policies.Any(p => p.Enabled))
|
if (policies.Any(p => p.Enabled))
|
||||||
@@ -252,7 +265,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPlan.HasSso && organization.UseSso)
|
if (!newPasswordManagerPlan.HasSso && organization.UseSso)
|
||||||
{
|
{
|
||||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
|
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
if (ssoConfig != null && ssoConfig.Enabled)
|
if (ssoConfig != null && ssoConfig.Enabled)
|
||||||
@@ -262,7 +275,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPlan.HasKeyConnector && organization.UseKeyConnector)
|
if (!newPasswordManagerPlan.HasKeyConnector && organization.UseKeyConnector)
|
||||||
{
|
{
|
||||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
|
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
if (ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector)
|
if (ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector)
|
||||||
@@ -272,7 +285,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPlan.HasResetPassword && organization.UseResetPassword)
|
if (!newPasswordManagerPlan.HasResetPassword && organization.UseResetPassword)
|
||||||
{
|
{
|
||||||
var resetPasswordPolicy =
|
var resetPasswordPolicy =
|
||||||
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||||
@@ -283,7 +296,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPlan.HasScim && organization.UseScim)
|
if (!newPasswordManagerPlan.HasScim && organization.UseScim)
|
||||||
{
|
{
|
||||||
var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
|
var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
|
||||||
OrganizationConnectionType.Scim);
|
OrganizationConnectionType.Scim);
|
||||||
@@ -294,7 +307,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPlan.HasCustomPermissions && organization.UseCustomPermissions)
|
if (!newPasswordManagerPlan.HasCustomPermissions && organization.UseCustomPermissions)
|
||||||
{
|
{
|
||||||
var organizationCustomUsers =
|
var organizationCustomUsers =
|
||||||
await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id,
|
await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id,
|
||||||
@@ -306,14 +319,25 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check storage?
|
if (upgrade.UseSecretsManager && newSecretsManagerPlan != null)
|
||||||
|
{
|
||||||
|
await ValidateSecretsManagerSeatsAndServiceAccountAsync(upgrade, organization, newSecretsManagerPlan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check storage?
|
||||||
string paymentIntentClientSecret = null;
|
string paymentIntentClientSecret = null;
|
||||||
var success = true;
|
var success = true;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||||
{
|
{
|
||||||
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization, newPlan,
|
var organizationUpgradePlan = upgrade.UseSecretsManager
|
||||||
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo);
|
? StaticStore.Plans.Where(p => p.Type == upgrade.Plan).ToList()
|
||||||
|
: StaticStore.Plans.Where(p => p.Type == upgrade.Plan && p.BitwardenProduct == BitwardenProductType.PasswordManager).ToList();
|
||||||
|
|
||||||
|
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization,
|
||||||
|
organizationUpgradePlan,
|
||||||
|
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo
|
||||||
|
, upgrade.AdditionalSmSeats.GetValueOrDefault(), upgrade.AdditionalServiceAccounts.GetValueOrDefault());
|
||||||
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
|
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -323,54 +347,100 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
organization.BusinessName = upgrade.BusinessName;
|
organization.BusinessName = upgrade.BusinessName;
|
||||||
organization.PlanType = newPlan.Type;
|
organization.PlanType = newPasswordManagerPlan.Type;
|
||||||
organization.Seats = (short)(newPlan.BaseSeats + upgrade.AdditionalSeats);
|
organization.Seats = (short)(newPasswordManagerPlan.BaseSeats + upgrade.AdditionalSeats);
|
||||||
organization.MaxCollections = newPlan.MaxCollections;
|
organization.MaxCollections = newPasswordManagerPlan.MaxCollections;
|
||||||
organization.UseGroups = newPlan.HasGroups;
|
organization.UseGroups = newPasswordManagerPlan.HasGroups;
|
||||||
organization.UseDirectory = newPlan.HasDirectory;
|
organization.UseDirectory = newPasswordManagerPlan.HasDirectory;
|
||||||
organization.UseEvents = newPlan.HasEvents;
|
organization.UseEvents = newPasswordManagerPlan.HasEvents;
|
||||||
organization.UseTotp = newPlan.HasTotp;
|
organization.UseTotp = newPasswordManagerPlan.HasTotp;
|
||||||
organization.Use2fa = newPlan.Has2fa;
|
organization.Use2fa = newPasswordManagerPlan.Has2fa;
|
||||||
organization.UseApi = newPlan.HasApi;
|
organization.UseApi = newPasswordManagerPlan.HasApi;
|
||||||
organization.SelfHost = newPlan.HasSelfHost;
|
organization.SelfHost = newPasswordManagerPlan.HasSelfHost;
|
||||||
organization.UsePolicies = newPlan.HasPolicies;
|
organization.UsePolicies = newPasswordManagerPlan.HasPolicies;
|
||||||
organization.MaxStorageGb = !newPlan.BaseStorageGb.HasValue ?
|
organization.MaxStorageGb = !newPasswordManagerPlan.BaseStorageGb.HasValue
|
||||||
(short?)null : (short)(newPlan.BaseStorageGb.Value + upgrade.AdditionalStorageGb);
|
? (short?)null
|
||||||
organization.UseGroups = newPlan.HasGroups;
|
: (short)(newPasswordManagerPlan.BaseStorageGb.Value + upgrade.AdditionalStorageGb);
|
||||||
organization.UseDirectory = newPlan.HasDirectory;
|
organization.UseGroups = newPasswordManagerPlan.HasGroups;
|
||||||
organization.UseEvents = newPlan.HasEvents;
|
organization.UseDirectory = newPasswordManagerPlan.HasDirectory;
|
||||||
organization.UseTotp = newPlan.HasTotp;
|
organization.UseEvents = newPasswordManagerPlan.HasEvents;
|
||||||
organization.Use2fa = newPlan.Has2fa;
|
organization.UseTotp = newPasswordManagerPlan.HasTotp;
|
||||||
organization.UseApi = newPlan.HasApi;
|
organization.Use2fa = newPasswordManagerPlan.Has2fa;
|
||||||
organization.UseSso = newPlan.HasSso;
|
organization.UseApi = newPasswordManagerPlan.HasApi;
|
||||||
organization.UseKeyConnector = newPlan.HasKeyConnector;
|
organization.UseSso = newPasswordManagerPlan.HasSso;
|
||||||
organization.UseScim = newPlan.HasScim;
|
organization.UseKeyConnector = newPasswordManagerPlan.HasKeyConnector;
|
||||||
organization.UseResetPassword = newPlan.HasResetPassword;
|
organization.UseScim = newPasswordManagerPlan.HasScim;
|
||||||
organization.SelfHost = newPlan.HasSelfHost;
|
organization.UseResetPassword = newPasswordManagerPlan.HasResetPassword;
|
||||||
organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
|
organization.SelfHost = newPasswordManagerPlan.HasSelfHost;
|
||||||
organization.UseCustomPermissions = newPlan.HasCustomPermissions;
|
organization.UsersGetPremium = newPasswordManagerPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
|
||||||
organization.Plan = newPlan.Name;
|
organization.UseCustomPermissions = newPasswordManagerPlan.HasCustomPermissions;
|
||||||
|
organization.Plan = newPasswordManagerPlan.Name;
|
||||||
organization.Enabled = success;
|
organization.Enabled = success;
|
||||||
organization.PublicKey = upgrade.PublicKey;
|
organization.PublicKey = upgrade.PublicKey;
|
||||||
organization.PrivateKey = upgrade.PrivateKey;
|
organization.PrivateKey = upgrade.PrivateKey;
|
||||||
|
organization.UsePasswordManager = true;
|
||||||
|
organization.SmSeats = (short)(newSecretsManagerPlan.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault());
|
||||||
|
organization.SmServiceAccounts = upgrade.AdditionalServiceAccounts.GetValueOrDefault();
|
||||||
|
organization.UseSecretsManager = upgrade.UseSecretsManager;
|
||||||
|
|
||||||
await ReplaceAndUpdateCacheAsync(organization);
|
await ReplaceAndUpdateCacheAsync(organization);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
await _referenceEventService.RaiseEventAsync(
|
await _referenceEventService.RaiseEventAsync(
|
||||||
new ReferenceEvent(ReferenceEventType.UpgradePlan, organization, _currentContext)
|
new ReferenceEvent(ReferenceEventType.UpgradePlan, organization, _currentContext)
|
||||||
{
|
{
|
||||||
PlanName = newPlan.Name,
|
PlanName = newPasswordManagerPlan.Name,
|
||||||
PlanType = newPlan.Type,
|
PlanType = newPasswordManagerPlan.Type,
|
||||||
OldPlanName = existingPlan.Name,
|
OldPlanName = existingPasswordManagerPlan.Name,
|
||||||
OldPlanType = existingPlan.Type,
|
OldPlanType = existingPasswordManagerPlan.Type,
|
||||||
Seats = organization.Seats,
|
Seats = organization.Seats,
|
||||||
Storage = organization.MaxStorageGb,
|
Storage = organization.MaxStorageGb,
|
||||||
|
SmSeats = organization.SmSeats,
|
||||||
|
ServiceAccounts = organization.SmServiceAccounts,
|
||||||
|
UseSecretsManager = organization.UseSecretsManager
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Tuple<bool, string>(success, paymentIntentClientSecret);
|
return new Tuple<bool, string>(success, paymentIntentClientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ValidateSecretsManagerSeatsAndServiceAccountAsync(OrganizationUpgrade upgrade, Organization organization,
|
||||||
|
Models.StaticStore.Plan newSecretsManagerPlan)
|
||||||
|
{
|
||||||
|
var newPlanSmSeats = (short)(newSecretsManagerPlan.BaseSeats +
|
||||||
|
(newSecretsManagerPlan.HasAdditionalSeatsOption
|
||||||
|
? upgrade.AdditionalSmSeats
|
||||||
|
: 0));
|
||||||
|
var occupiedSmSeats =
|
||||||
|
await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
|
if (!organization.SmSeats.HasValue || organization.SmSeats.Value > newPlanSmSeats)
|
||||||
|
{
|
||||||
|
if (occupiedSmSeats > newPlanSmSeats)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(
|
||||||
|
$"Your organization currently has {occupiedSmSeats} Secrets Manager seats filled. " +
|
||||||
|
$"Your new plan only has ({newPlanSmSeats}) seats. Remove some users.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSecretsManagerPlan.BaseServiceAccount != null)
|
||||||
|
{
|
||||||
|
if (!organization.SmServiceAccounts.HasValue ||
|
||||||
|
organization.SmServiceAccounts.Value > newSecretsManagerPlan.MaxServiceAccount)
|
||||||
|
{
|
||||||
|
var currentServiceAccounts =
|
||||||
|
await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
|
||||||
|
if (currentServiceAccounts > newSecretsManagerPlan.MaxServiceAccount)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(
|
||||||
|
$"Your organization currently has {currentServiceAccounts} service account seats filled. " +
|
||||||
|
$"Your new plan only has ({newSecretsManagerPlan.MaxServiceAccount}) service accounts. Remove some service accounts.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
|
public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
|
||||||
{
|
{
|
||||||
var organization = await GetOrgById(organizationId);
|
var organization = await GetOrgById(organizationId);
|
||||||
@@ -607,15 +677,14 @@ public class OrganizationService : IOrganizationService
|
|||||||
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup,
|
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup,
|
||||||
bool provider = false)
|
bool provider = false)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == signup.Plan);
|
var passwordManagerPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == signup.Plan);
|
||||||
if (plan is not { LegacyYear: null })
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid plan selected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plan.Disabled)
|
ValidatePasswordManagerPlan(passwordManagerPlan, signup);
|
||||||
|
|
||||||
|
var secretsManagerPlan = StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == signup.Plan);
|
||||||
|
if (signup.UseSecretsManager)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Plan not found.");
|
ValidateSecretsManagerPlan(secretsManagerPlan, signup);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!provider)
|
if (!provider)
|
||||||
@@ -623,8 +692,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
|
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateOrganizationUpgradeParameters(plan, signup);
|
|
||||||
|
|
||||||
var organization = new Organization
|
var organization = new Organization
|
||||||
{
|
{
|
||||||
// Pre-generate the org id so that we can save it with the Stripe subscription..
|
// Pre-generate the org id so that we can save it with the Stripe subscription..
|
||||||
@@ -632,25 +699,25 @@ public class OrganizationService : IOrganizationService
|
|||||||
Name = signup.Name,
|
Name = signup.Name,
|
||||||
BillingEmail = signup.BillingEmail,
|
BillingEmail = signup.BillingEmail,
|
||||||
BusinessName = signup.BusinessName,
|
BusinessName = signup.BusinessName,
|
||||||
PlanType = plan.Type,
|
PlanType = passwordManagerPlan.Type,
|
||||||
Seats = (short)(plan.BaseSeats + signup.AdditionalSeats),
|
Seats = (short)(passwordManagerPlan.BaseSeats + signup.AdditionalSeats),
|
||||||
MaxCollections = plan.MaxCollections,
|
MaxCollections = passwordManagerPlan.MaxCollections,
|
||||||
MaxStorageGb = !plan.BaseStorageGb.HasValue ?
|
MaxStorageGb = !passwordManagerPlan.BaseStorageGb.HasValue ?
|
||||||
(short?)null : (short)(plan.BaseStorageGb.Value + signup.AdditionalStorageGb),
|
(short?)null : (short)(passwordManagerPlan.BaseStorageGb.Value + signup.AdditionalStorageGb),
|
||||||
UsePolicies = plan.HasPolicies,
|
UsePolicies = passwordManagerPlan.HasPolicies,
|
||||||
UseSso = plan.HasSso,
|
UseSso = passwordManagerPlan.HasSso,
|
||||||
UseGroups = plan.HasGroups,
|
UseGroups = passwordManagerPlan.HasGroups,
|
||||||
UseEvents = plan.HasEvents,
|
UseEvents = passwordManagerPlan.HasEvents,
|
||||||
UseDirectory = plan.HasDirectory,
|
UseDirectory = passwordManagerPlan.HasDirectory,
|
||||||
UseTotp = plan.HasTotp,
|
UseTotp = passwordManagerPlan.HasTotp,
|
||||||
Use2fa = plan.Has2fa,
|
Use2fa = passwordManagerPlan.Has2fa,
|
||||||
UseApi = plan.HasApi,
|
UseApi = passwordManagerPlan.HasApi,
|
||||||
UseResetPassword = plan.HasResetPassword,
|
UseResetPassword = passwordManagerPlan.HasResetPassword,
|
||||||
SelfHost = plan.HasSelfHost,
|
SelfHost = passwordManagerPlan.HasSelfHost,
|
||||||
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
|
UsersGetPremium = passwordManagerPlan.UsersGetPremium || signup.PremiumAccessAddon,
|
||||||
UseCustomPermissions = plan.HasCustomPermissions,
|
UseCustomPermissions = passwordManagerPlan.HasCustomPermissions,
|
||||||
UseScim = plan.HasScim,
|
UseScim = passwordManagerPlan.HasScim,
|
||||||
Plan = plan.Name,
|
Plan = passwordManagerPlan.Name,
|
||||||
Gateway = null,
|
Gateway = null,
|
||||||
ReferenceData = signup.Owner.ReferenceData,
|
ReferenceData = signup.Owner.ReferenceData,
|
||||||
Enabled = true,
|
Enabled = true,
|
||||||
@@ -659,10 +726,14 @@ public class OrganizationService : IOrganizationService
|
|||||||
PrivateKey = signup.PrivateKey,
|
PrivateKey = signup.PrivateKey,
|
||||||
CreationDate = DateTime.UtcNow,
|
CreationDate = DateTime.UtcNow,
|
||||||
RevisionDate = DateTime.UtcNow,
|
RevisionDate = DateTime.UtcNow,
|
||||||
Status = OrganizationStatusType.Created
|
Status = OrganizationStatusType.Created,
|
||||||
|
UsePasswordManager = true,
|
||||||
|
SmSeats = (short)(secretsManagerPlan.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault()),
|
||||||
|
SmServiceAccounts = signup.AdditionalServiceAccounts.GetValueOrDefault(),
|
||||||
|
UseSecretsManager = signup.UseSecretsManager
|
||||||
};
|
};
|
||||||
|
|
||||||
if (plan.Type == PlanType.Free && !provider)
|
if (passwordManagerPlan.Type == PlanType.Free && !provider)
|
||||||
{
|
{
|
||||||
var adminCount =
|
var adminCount =
|
||||||
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
||||||
@@ -671,11 +742,16 @@ public class OrganizationService : IOrganizationService
|
|||||||
throw new BadRequestException("You can only be an admin of one free organization.");
|
throw new BadRequestException("You can only be an admin of one free organization.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (plan.Type != PlanType.Free)
|
else if (passwordManagerPlan.Type != PlanType.Free)
|
||||||
{
|
{
|
||||||
|
var purchaseOrganizationPlan = signup.UseSecretsManager
|
||||||
|
? StaticStore.Plans.Where(p => p.Type == signup.Plan).ToList()
|
||||||
|
: StaticStore.PasswordManagerPlans.Where(p => p.Type == signup.Plan).Take(1).ToList();
|
||||||
|
|
||||||
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
||||||
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
signup.PaymentToken, purchaseOrganizationPlan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
||||||
signup.PremiumAccessAddon, signup.TaxInfo, provider);
|
signup.PremiumAccessAddon, signup.TaxInfo, provider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||||
|
signup.AdditionalServiceAccounts.GetValueOrDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
var ownerId = provider ? default : signup.Owner.Id;
|
var ownerId = provider ? default : signup.Owner.Id;
|
||||||
@@ -683,10 +759,13 @@ public class OrganizationService : IOrganizationService
|
|||||||
await _referenceEventService.RaiseEventAsync(
|
await _referenceEventService.RaiseEventAsync(
|
||||||
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
|
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
|
||||||
{
|
{
|
||||||
PlanName = plan.Name,
|
PlanName = passwordManagerPlan.Name,
|
||||||
PlanType = plan.Type,
|
PlanType = passwordManagerPlan.Type,
|
||||||
Seats = returnValue.Item1.Seats,
|
Seats = returnValue.Item1.Seats,
|
||||||
Storage = returnValue.Item1.MaxStorageGb,
|
Storage = returnValue.Item1.MaxStorageGb,
|
||||||
|
SmSeats = returnValue.Item1.SmSeats,
|
||||||
|
ServiceAccounts = returnValue.Item1.SmServiceAccounts,
|
||||||
|
UseSecretsManager = returnValue.Item1.UseSecretsManager
|
||||||
});
|
});
|
||||||
return returnValue;
|
return returnValue;
|
||||||
}
|
}
|
||||||
@@ -2060,8 +2139,43 @@ public class OrganizationService : IOrganizationService
|
|||||||
return await _organizationRepository.GetByIdAsync(id);
|
return await _organizationRepository.GetByIdAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateOrganizationUpgradeParameters(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
|
private static void ValidatePlan(Models.StaticStore.Plan plan, int additionalSeats, string productType)
|
||||||
{
|
{
|
||||||
|
if (plan is not { LegacyYear: null })
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Invalid {productType} plan selected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.Disabled)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"{productType} Plan not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.BaseSeats + additionalSeats <= 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"You do not have any {productType} seats!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalSeats < 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"You can't subtract {productType} seats!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
|
||||||
|
{
|
||||||
|
ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager");
|
||||||
|
|
||||||
|
if (plan.BaseSeats + upgrade.AdditionalSeats <= 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"You do not have any Password Manager seats!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upgrade.AdditionalSeats < 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"You can't subtract Password Manager seats!");
|
||||||
|
}
|
||||||
|
|
||||||
if (!plan.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)
|
if (!plan.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Plan does not allow additional storage.");
|
throw new BadRequestException("Plan does not allow additional storage.");
|
||||||
@@ -2077,16 +2191,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
|
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plan.BaseSeats + upgrade.AdditionalSeats <= 0)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You do not have any seats!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (upgrade.AdditionalSeats < 0)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You can't subtract seats!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!plan.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)
|
if (!plan.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Plan does not allow additional users.");
|
throw new BadRequestException("Plan does not allow additional users.");
|
||||||
@@ -2100,6 +2204,36 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
|
||||||
|
{
|
||||||
|
ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), "Secrets Manager");
|
||||||
|
|
||||||
|
if (!plan.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Plan does not allow additional Service Accounts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot have more Secrets Manager seats than Password Manager seats.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You can't subtract Service Accounts!");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (plan.HasAdditionalSeatsOption)
|
||||||
|
{
|
||||||
|
case false when upgrade.AdditionalSmSeats > 0:
|
||||||
|
throw new BadRequestException("Plan does not allow additional users.");
|
||||||
|
case true when plan.MaxAdditionalSeats.HasValue &&
|
||||||
|
upgrade.AdditionalSmSeats > plan.MaxAdditionalSeats.Value:
|
||||||
|
throw new BadRequestException($"Selected plan allows a maximum of " +
|
||||||
|
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType, Permissions permissions)
|
private async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType, Permissions permissions)
|
||||||
{
|
{
|
||||||
if (await _currentContext.OrganizationOwner(organizationId))
|
if (await _currentContext.OrganizationOwner(organizationId))
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||||
string paymentToken, StaticStore.Plan plan, short additionalStorageGb,
|
string paymentToken, List<StaticStore.Plan> plans, short additionalStorageGb,
|
||||||
int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false)
|
int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false,
|
||||||
|
int additionalSmSeats = 0, int additionalServiceAccount = 0)
|
||||||
{
|
{
|
||||||
Braintree.Customer braintreeCustomer = null;
|
Braintree.Customer braintreeCustomer = null;
|
||||||
string stipeCustomerSourceToken = null;
|
string stipeCustomerSourceToken = null;
|
||||||
@@ -110,7 +111,8 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon);
|
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plans, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
|
||||||
|
, additionalSmSeats, additionalServiceAccount);
|
||||||
|
|
||||||
Stripe.Customer customer = null;
|
Stripe.Customer customer = null;
|
||||||
Stripe.Subscription subscription;
|
Stripe.Subscription subscription;
|
||||||
@@ -221,8 +223,9 @@ public class StripePaymentService : IPaymentService
|
|||||||
public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) =>
|
public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) =>
|
||||||
ChangeOrganizationSponsorship(org, sponsorship, false);
|
ChangeOrganizationSponsorship(org, sponsorship, false);
|
||||||
|
|
||||||
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan,
|
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, List<StaticStore.Plan> plans,
|
||||||
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
|
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo,
|
||||||
|
int additionalSmSeats = 0, int additionalServiceAccounts = 0)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
|
if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
|
||||||
{
|
{
|
||||||
@@ -255,7 +258,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon);
|
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plans, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon);
|
||||||
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
|
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
|
||||||
|
|
||||||
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
|
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
|
||||||
|
|||||||
@@ -70,4 +70,7 @@ public class ReferenceEvent
|
|||||||
|
|
||||||
public string ClientId { get; set; }
|
public string ClientId { get; set; }
|
||||||
public Version ClientVersion { get; set; }
|
public Version ClientVersion { get; set; }
|
||||||
|
public int? SmSeats { get; set; }
|
||||||
|
public int? ServiceAccounts { get; set; }
|
||||||
|
public bool UseSecretsManager { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ public class Startup
|
|||||||
// Repositories
|
// Repositories
|
||||||
services.AddDatabaseRepositories(globalSettings);
|
services.AddDatabaseRepositories(globalSettings);
|
||||||
|
|
||||||
|
services.AddOosServices();
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
services.AddScoped<ICurrentContext, CurrentContext>();
|
services.AddScoped<ICurrentContext, CurrentContext>();
|
||||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
@@ -151,6 +153,7 @@ public class Startup
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
IApplicationBuilder app,
|
IApplicationBuilder app,
|
||||||
IWebHostEnvironment env,
|
IWebHostEnvironment env,
|
||||||
|
|||||||
@@ -99,6 +99,19 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var result = await connection.ExecuteScalarAsync<int>(
|
||||||
|
"[dbo].[OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId]",
|
||||||
|
new { OrganizationId = organizationId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails,
|
public async Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails,
|
||||||
bool onlyRegisteredUsers)
|
bool onlyRegisteredUsers)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -621,4 +621,11 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
|||||||
return await query.ToListAsync();
|
return await query.ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
var query = new OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery(organizationId);
|
||||||
|
return await GetCountFromQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Models;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||||
|
|
||||||
|
public class OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery : IQuery<OrganizationUser>
|
||||||
|
{
|
||||||
|
private readonly Guid _organizationId;
|
||||||
|
|
||||||
|
public OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery(Guid organizationId)
|
||||||
|
{
|
||||||
|
_organizationId = organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)
|
||||||
|
{
|
||||||
|
var query = from ou in dbContext.OrganizationUsers
|
||||||
|
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited && ou.AccessSecretsManager == true
|
||||||
|
select ou;
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ using Bit.Core.IdentityServer;
|
|||||||
using Bit.Core.OrganizationFeatures;
|
using Bit.Core.OrganizationFeatures;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Resources;
|
using Bit.Core.Resources;
|
||||||
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
@@ -329,6 +330,7 @@ public static class ServiceCollectionExtensions
|
|||||||
public static void AddOosServices(this IServiceCollection services)
|
public static void AddOosServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IProviderService, NoopProviderService>();
|
services.AddScoped<IProviderService, NoopProviderService>();
|
||||||
|
services.AddScoped<IServiceAccountRepository, NoopRepos.NoopServiceAccountRepository>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddNoopServices(this IServiceCollection services)
|
public static void AddNoopServices(this IServiceCollection services)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(1)
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUserView]
|
||||||
|
WHERE
|
||||||
|
OrganizationId = @OrganizationId
|
||||||
|
AND Status >= 0 --Invited
|
||||||
|
AND AccessSecretsManager = 1
|
||||||
|
END
|
||||||
|
GO
|
||||||
@@ -13,6 +13,7 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
using Bit.Core.Models.StaticStore;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@@ -22,6 +23,7 @@ using Bit.Core.Test.AutoFixture.PolicyFixtures;
|
|||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
@@ -174,6 +176,20 @@ public class OrganizationServiceTests
|
|||||||
Assert.Contains("already on this plan", exception.Message);
|
Assert.Contains("already on this plan", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpgradePlan_SM_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
upgrade.Plan = organization.PlanType;
|
||||||
|
upgrade.UseSecretsManager = true;
|
||||||
|
upgrade.AdditionalSmSeats = 10;
|
||||||
|
upgrade.AdditionalServiceAccounts = 10;
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
|
||||||
|
Assert.Contains("already on this plan", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
|
[Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
|
||||||
public async Task UpgradePlan_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
|
public async Task UpgradePlan_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
|
||||||
SutProvider<OrganizationService> sutProvider)
|
SutProvider<OrganizationService> sutProvider)
|
||||||
@@ -184,16 +200,147 @@ public class OrganizationServiceTests
|
|||||||
Assert.Contains("can only upgrade", exception.Message);
|
Assert.Contains("can only upgrade", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
|
||||||
|
public async Task UpgradePlan_SM_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
upgrade.UseSecretsManager = true;
|
||||||
|
upgrade.AdditionalSmSeats = 10;
|
||||||
|
upgrade.AdditionalServiceAccounts = 10;
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
|
||||||
|
Assert.Contains("can only upgrade", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
||||||
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
|
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
|
||||||
SutProvider<OrganizationService> sutProvider)
|
SutProvider<OrganizationService> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
upgrade.AdditionalSmSeats = 10;
|
||||||
|
upgrade.AdditionalSeats = 10;
|
||||||
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
||||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organization);
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SignUp_SM_Passes(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
signup.AdditionalSmSeats = 10;
|
||||||
|
signup.AdditionalSeats = 10;
|
||||||
|
signup.Plan = PlanType.EnterpriseAnnually;
|
||||||
|
signup.UseSecretsManager = true;
|
||||||
|
signup.PaymentMethodType = PaymentMethodType.Card;
|
||||||
|
signup.PremiumAccessAddon = false;
|
||||||
|
|
||||||
|
var purchaseOrganizationPlan = StaticStore.Plans.Where(x => x.Type == signup.Plan).ToList();
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.SignUpAsync(signup);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IReferenceEventService>().Received(1)
|
||||||
|
.RaiseEventAsync(Arg.Is<ReferenceEvent>(referenceEvent =>
|
||||||
|
referenceEvent.Type == ReferenceEventType.Signup &&
|
||||||
|
referenceEvent.PlanName == purchaseOrganizationPlan[0].Name &&
|
||||||
|
referenceEvent.PlanType == purchaseOrganizationPlan[0].Type &&
|
||||||
|
referenceEvent.Seats == result.Item1.Seats &&
|
||||||
|
referenceEvent.SmSeats == result.Item1.SmSeats &&
|
||||||
|
referenceEvent.ServiceAccounts == result.Item1.SmServiceAccounts &&
|
||||||
|
referenceEvent.UseSecretsManager == result.Item1.UseSecretsManager &&
|
||||||
|
referenceEvent.Storage == result.Item1.MaxStorageGb));
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.NotNull(result.Item1);
|
||||||
|
Assert.NotNull(result.Item2);
|
||||||
|
Assert.IsType<Tuple<Organization, OrganizationUser>>(result);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IPaymentService>().Received(1).PurchaseOrganizationAsync(
|
||||||
|
Arg.Any<Organization>(),
|
||||||
|
signup.PaymentMethodType.Value,
|
||||||
|
signup.PaymentToken,
|
||||||
|
Arg.Is<List<Plan>>(plan => plan.All(p => purchaseOrganizationPlan.Contains(p))),
|
||||||
|
signup.AdditionalStorageGb,
|
||||||
|
signup.AdditionalSeats,
|
||||||
|
signup.PremiumAccessAddon,
|
||||||
|
signup.TaxInfo,
|
||||||
|
false,
|
||||||
|
signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||||
|
signup.AdditionalServiceAccounts.GetValueOrDefault()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SignUpAsync_SecretManagerValidation_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
signup.AdditionalSmSeats = 10;
|
||||||
|
signup.AdditionalSeats = 10;
|
||||||
|
signup.Plan = PlanType.EnterpriseAnnually;
|
||||||
|
signup.UseSecretsManager = true;
|
||||||
|
signup.PaymentMethodType = PaymentMethodType.Card;
|
||||||
|
signup.PremiumAccessAddon = false;
|
||||||
|
signup.AdditionalServiceAccounts = 10;
|
||||||
|
|
||||||
|
var purchaseOrganizationPlan = StaticStore.Plans.Where(x => x.Type == signup.Plan && x.BitwardenProduct == BitwardenProductType.PasswordManager).ToList();
|
||||||
|
var secretsManagerPlan = StaticStore.GetSecretsManagerPlan(PlanType.EnterpriseAnnually);
|
||||||
|
secretsManagerPlan.HasAdditionalServiceAccountOption = false;
|
||||||
|
purchaseOrganizationPlan.Add(secretsManagerPlan);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.SignUpAsync(signup));
|
||||||
|
Assert.Contains("Plan does not allow additional Service Accounts.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SignUpAsync_SMSeatsGreatThanPMSeat_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
signup.AdditionalSmSeats = 100;
|
||||||
|
signup.AdditionalSeats = 10;
|
||||||
|
signup.Plan = PlanType.EnterpriseAnnually;
|
||||||
|
signup.UseSecretsManager = true;
|
||||||
|
signup.PaymentMethodType = PaymentMethodType.Card;
|
||||||
|
signup.PremiumAccessAddon = false;
|
||||||
|
signup.AdditionalServiceAccounts = 10;
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.SignUpAsync(signup));
|
||||||
|
Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SignUpAsync_InvalidateServiceAccount_ShouldThrowException(OrganizationSignup signup, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
signup.AdditionalSmSeats = 10;
|
||||||
|
signup.AdditionalSeats = 10;
|
||||||
|
signup.Plan = PlanType.EnterpriseAnnually;
|
||||||
|
signup.UseSecretsManager = true;
|
||||||
|
signup.PaymentMethodType = PaymentMethodType.Card;
|
||||||
|
signup.PremiumAccessAddon = false;
|
||||||
|
signup.AdditionalServiceAccounts = -10;
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.SignUpAsync(signup));
|
||||||
|
Assert.Contains("You can't subtract Service Accounts!", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
||||||
|
public async Task UpgradePlan_SM_Passes(Organization organization, OrganizationUpgrade upgrade,
|
||||||
|
SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
upgrade.AdditionalSmSeats = 10;
|
||||||
|
upgrade.AdditionalSeats = 10;
|
||||||
|
var result = await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organization);
|
||||||
|
Assert.True(result.Item1);
|
||||||
|
Assert.NotNull(result.Item2);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
|
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
|
||||||
InvitorUserType = OrganizationUserType.Owner), BitAutoData]
|
InvitorUserType = OrganizationUserType.Owner), BitAutoData]
|
||||||
@@ -1469,4 +1616,5 @@ public class OrganizationServiceTests
|
|||||||
|
|
||||||
Assert.Equal(includeProvider, result);
|
Assert.Equal(includeProvider, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class StripePaymentServiceTests
|
|||||||
public async void PurchaseOrganizationAsync_Invalid(PaymentMethodType paymentMethodType, SutProvider<StripePaymentService> sutProvider)
|
public async void PurchaseOrganizationAsync_Invalid(PaymentMethodType paymentMethodType, SutProvider<StripePaymentService> sutProvider)
|
||||||
{
|
{
|
||||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||||
() => sutProvider.Sut.PurchaseOrganizationAsync(null, paymentMethodType, null, null, 0, 0, false, null));
|
() => sutProvider.Sut.PurchaseOrganizationAsync(null, paymentMethodType, null, null, 0, 0, false, null, false, -1, -1));
|
||||||
|
|
||||||
Assert.Equal("Payment method is not supported at this time.", exception.Message);
|
Assert.Equal("Payment method is not supported at this time.", exception.Message);
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ public class StripePaymentServiceTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void PurchaseOrganizationAsync_Stripe_ProviderOrg_Coupon_Add(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo, bool provider = true)
|
public async void PurchaseOrganizationAsync_Stripe_ProviderOrg_Coupon_Add(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo, bool provider = true)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually);
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
@@ -52,7 +52,7 @@ public class StripePaymentServiceTests
|
|||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||||
});
|
});
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo, provider);
|
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plans, 0, 0, false, taxInfo, provider);
|
||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||||
@@ -86,10 +86,63 @@ public class StripePaymentServiceTests
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void PurchaseOrganizationAsync_SM_Stripe_ProviderOrg_Coupon_Add(SutProvider<StripePaymentService> sutProvider, Organization organization,
|
||||||
|
string paymentToken, TaxInfo taxInfo, bool provider = true)
|
||||||
|
{
|
||||||
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
|
{
|
||||||
|
Id = "C-1",
|
||||||
|
});
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||||
|
{
|
||||||
|
Id = "S-1",
|
||||||
|
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plans, 1, 1,
|
||||||
|
false, taxInfo, provider, 1, 1);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||||
|
Assert.Equal("C-1", organization.GatewayCustomerId);
|
||||||
|
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
||||||
|
Assert.True(organization.Enabled);
|
||||||
|
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
||||||
|
|
||||||
|
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
||||||
|
c.Description == organization.BusinessName &&
|
||||||
|
c.Email == organization.BillingEmail &&
|
||||||
|
c.Source == paymentToken &&
|
||||||
|
c.PaymentMethod == null &&
|
||||||
|
c.Coupon == "msp-discount-35" &&
|
||||||
|
!c.Metadata.Any() &&
|
||||||
|
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
||||||
|
c.Address.Country == taxInfo.BillingAddressCountry &&
|
||||||
|
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||||
|
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||||
|
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||||
|
c.Address.City == taxInfo.BillingAddressCity &&
|
||||||
|
c.Address.State == taxInfo.BillingAddressState &&
|
||||||
|
c.TaxIdData == null
|
||||||
|
));
|
||||||
|
|
||||||
|
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
||||||
|
s.Customer == "C-1" &&
|
||||||
|
s.Expand[0] == "latest_invoice.payment_intent" &&
|
||||||
|
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
||||||
|
s.Items.Count == 4
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void PurchaseOrganizationAsync_Stripe(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
public async void PurchaseOrganizationAsync_Stripe(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually);
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
@@ -102,7 +155,8 @@ public class StripePaymentServiceTests
|
|||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||||
});
|
});
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo);
|
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plans, 0, 0
|
||||||
|
, false, taxInfo, false, 8, 10);
|
||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||||
@@ -110,7 +164,6 @@ public class StripePaymentServiceTests
|
|||||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
||||||
Assert.True(organization.Enabled);
|
Assert.True(organization.Enabled);
|
||||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
||||||
var res = organization.SubscriberName();
|
|
||||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
||||||
c.Description == organization.BusinessName &&
|
c.Description == organization.BusinessName &&
|
||||||
c.Email == organization.BillingEmail &&
|
c.Email == organization.BillingEmail &&
|
||||||
@@ -134,67 +187,14 @@ public class StripePaymentServiceTests
|
|||||||
s.Customer == "C-1" &&
|
s.Customer == "C-1" &&
|
||||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
s.Expand[0] == "latest_invoice.payment_intent" &&
|
||||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
||||||
s.Items.Count == 0
|
s.Items.Count == 2
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async void PurchaseOrganizationAsync_Stripe_PM(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually);
|
|
||||||
paymentToken = "pm_" + paymentToken;
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
});
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo);
|
|
||||||
|
|
||||||
Assert.Null(result);
|
|
||||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
|
||||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
|
||||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
|
||||||
Assert.True(organization.Enabled);
|
|
||||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
|
||||||
|
|
||||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
|
||||||
c.Description == organization.BusinessName &&
|
|
||||||
c.Email == organization.BillingEmail &&
|
|
||||||
c.Source == null &&
|
|
||||||
c.PaymentMethod == paymentToken &&
|
|
||||||
!c.Metadata.Any() &&
|
|
||||||
c.InvoiceSettings.DefaultPaymentMethod == paymentToken &&
|
|
||||||
c.InvoiceSettings.CustomFields != null &&
|
|
||||||
c.InvoiceSettings.CustomFields[0].Name == "Organization" &&
|
|
||||||
c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) &&
|
|
||||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
|
||||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
|
||||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
|
||||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
|
||||||
c.Address.City == taxInfo.BillingAddressCity &&
|
|
||||||
c.Address.State == taxInfo.BillingAddressState &&
|
|
||||||
c.TaxIdData == null
|
|
||||||
));
|
|
||||||
|
|
||||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
|
||||||
s.Customer == "C-1" &&
|
|
||||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
|
||||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
|
||||||
s.Items.Count == 0
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void PurchaseOrganizationAsync_Stripe_TaxRate(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
public async void PurchaseOrganizationAsync_Stripe_TaxRate(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually);
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
@@ -210,7 +210,37 @@ public class StripePaymentServiceTests
|
|||||||
t.Country == taxInfo.BillingAddressCountry && t.PostalCode == taxInfo.BillingAddressPostalCode))
|
t.Country == taxInfo.BillingAddressCountry && t.PostalCode == taxInfo.BillingAddressPostalCode))
|
||||||
.Returns(new List<TaxRate> { new() { Id = "T-1" } });
|
.Returns(new List<TaxRate> { new() { Id = "T-1" } });
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo);
|
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plans, 0, 0, false, taxInfo);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
|
||||||
|
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
||||||
|
s.DefaultTaxRates.Count == 1 &&
|
||||||
|
s.DefaultTaxRates[0] == "T-1"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void PurchaseOrganizationAsync_Stripe_TaxRate_SM(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
|
{
|
||||||
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
|
{
|
||||||
|
Id = "C-1",
|
||||||
|
});
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||||
|
{
|
||||||
|
Id = "S-1",
|
||||||
|
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||||
|
});
|
||||||
|
sutProvider.GetDependency<ITaxRateRepository>().GetByLocationAsync(Arg.Is<TaxRate>(t =>
|
||||||
|
t.Country == taxInfo.BillingAddressCountry && t.PostalCode == taxInfo.BillingAddressPostalCode))
|
||||||
|
.Returns(new List<TaxRate> { new() { Id = "T-1" } });
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plans, 2, 2,
|
||||||
|
false, taxInfo, false, 2, 2);
|
||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
|
|
||||||
@@ -223,7 +253,7 @@ public class StripePaymentServiceTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void PurchaseOrganizationAsync_Stripe_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
public async void PurchaseOrganizationAsync_Stripe_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually);
|
var plan = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
paymentToken = "pm_" + paymentToken;
|
paymentToken = "pm_" + paymentToken;
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
@@ -253,10 +283,44 @@ public class StripePaymentServiceTests
|
|||||||
await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
|
await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void PurchaseOrganizationAsync_SM_Stripe_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
|
{
|
||||||
|
var plan = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
paymentToken = "pm_" + paymentToken;
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
|
{
|
||||||
|
Id = "C-1",
|
||||||
|
});
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||||
|
{
|
||||||
|
Id = "S-1",
|
||||||
|
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||||
|
Status = "incomplete",
|
||||||
|
LatestInvoice = new Stripe.Invoice
|
||||||
|
{
|
||||||
|
PaymentIntent = new Stripe.PaymentIntent
|
||||||
|
{
|
||||||
|
Status = "requires_payment_method",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||||
|
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan,
|
||||||
|
1, 12, false, taxInfo, false, 10, 10));
|
||||||
|
|
||||||
|
Assert.Equal("Payment method was declined.", exception.Message);
|
||||||
|
|
||||||
|
await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void PurchaseOrganizationAsync_Stripe_RequiresAction(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
public async void PurchaseOrganizationAsync_Stripe_RequiresAction(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually);
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
@@ -278,7 +342,39 @@ public class StripePaymentServiceTests
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo);
|
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plans, 0, 0, false, taxInfo);
|
||||||
|
|
||||||
|
Assert.Equal("clientSecret", result);
|
||||||
|
Assert.False(organization.Enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void PurchaseOrganizationAsync_SM_Stripe_RequiresAction(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
|
{
|
||||||
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
|
{
|
||||||
|
Id = "C-1",
|
||||||
|
});
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||||
|
{
|
||||||
|
Id = "S-1",
|
||||||
|
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||||
|
Status = "incomplete",
|
||||||
|
LatestInvoice = new Stripe.Invoice
|
||||||
|
{
|
||||||
|
PaymentIntent = new Stripe.PaymentIntent
|
||||||
|
{
|
||||||
|
Status = "requires_action",
|
||||||
|
ClientSecret = "clientSecret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plans,
|
||||||
|
10, 10, false, taxInfo, false, 10, 10);
|
||||||
|
|
||||||
Assert.Equal("clientSecret", result);
|
Assert.Equal("clientSecret", result);
|
||||||
Assert.False(organization.Enabled);
|
Assert.False(organization.Enabled);
|
||||||
@@ -287,7 +383,7 @@ public class StripePaymentServiceTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void PurchaseOrganizationAsync_Paypal(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
public async void PurchaseOrganizationAsync_Paypal(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually);
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
@@ -310,7 +406,7 @@ public class StripePaymentServiceTests
|
|||||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
||||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo);
|
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plans, 0, 0, false, taxInfo);
|
||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||||
@@ -343,10 +439,70 @@ public class StripePaymentServiceTests
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void PurchaseOrganizationAsync_SM_Paypal(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
|
{
|
||||||
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
|
{
|
||||||
|
Id = "C-1",
|
||||||
|
});
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
||||||
|
{
|
||||||
|
Id = "S-1",
|
||||||
|
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
||||||
|
});
|
||||||
|
|
||||||
|
var customer = Substitute.For<Customer>();
|
||||||
|
customer.Id.ReturnsForAnyArgs("Braintree-Id");
|
||||||
|
customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For<PaymentMethod>() });
|
||||||
|
var customerResult = Substitute.For<Result<Customer>>();
|
||||||
|
customerResult.IsSuccess().Returns(true);
|
||||||
|
customerResult.Target.ReturnsForAnyArgs(customer);
|
||||||
|
|
||||||
|
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
||||||
|
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plans,
|
||||||
|
2, 10, false, taxInfo, false, 10, 10);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
||||||
|
Assert.Equal("C-1", organization.GatewayCustomerId);
|
||||||
|
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
||||||
|
Assert.True(organization.Enabled);
|
||||||
|
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
||||||
|
|
||||||
|
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
||||||
|
c.Description == organization.BusinessName &&
|
||||||
|
c.Email == organization.BillingEmail &&
|
||||||
|
c.PaymentMethod == null &&
|
||||||
|
c.Metadata.Count == 1 &&
|
||||||
|
c.Metadata["btCustomerId"] == "Braintree-Id" &&
|
||||||
|
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
||||||
|
c.Address.Country == taxInfo.BillingAddressCountry &&
|
||||||
|
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||||
|
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||||
|
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||||
|
c.Address.City == taxInfo.BillingAddressCity &&
|
||||||
|
c.Address.State == taxInfo.BillingAddressState &&
|
||||||
|
c.TaxIdData == null
|
||||||
|
));
|
||||||
|
|
||||||
|
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
||||||
|
s.Customer == "C-1" &&
|
||||||
|
s.Expand[0] == "latest_invoice.payment_intent" &&
|
||||||
|
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
||||||
|
s.Items.Count > 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void PurchaseOrganizationAsync_Paypal_FailedCreate(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
public async void PurchaseOrganizationAsync_Paypal_FailedCreate(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually);
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
var customerResult = Substitute.For<Result<Customer>>();
|
var customerResult = Substitute.For<Result<Customer>>();
|
||||||
customerResult.IsSuccess().Returns(false);
|
customerResult.IsSuccess().Returns(false);
|
||||||
@@ -355,7 +511,25 @@ public class StripePaymentServiceTests
|
|||||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo));
|
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plans, 0, 0, false, taxInfo));
|
||||||
|
|
||||||
|
Assert.Equal("Failed to create PayPal customer record.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void PurchaseOrganizationAsync_SM_Paypal_FailedCreate(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
|
{
|
||||||
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
|
||||||
|
var customerResult = Substitute.For<Result<Customer>>();
|
||||||
|
customerResult.IsSuccess().Returns(false);
|
||||||
|
|
||||||
|
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
||||||
|
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||||
|
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plans,
|
||||||
|
1, 1, false, taxInfo, false, 8, 8));
|
||||||
|
|
||||||
Assert.Equal("Failed to create PayPal customer record.", exception.Message);
|
Assert.Equal("Failed to create PayPal customer record.", exception.Message);
|
||||||
}
|
}
|
||||||
@@ -363,7 +537,7 @@ public class StripePaymentServiceTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async void PurchaseOrganizationAsync_PayPal_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
public async void PurchaseOrganizationAsync_PayPal_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually);
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
paymentToken = "pm_" + paymentToken;
|
paymentToken = "pm_" + paymentToken;
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
@@ -396,7 +570,7 @@ public class StripePaymentServiceTests
|
|||||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
var exception = await Assert.ThrowsAsync<GatewayException>(
|
||||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo));
|
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plans, 0, 0, false, taxInfo));
|
||||||
|
|
||||||
Assert.Equal("Payment method was declined.", exception.Message);
|
Assert.Equal("Payment method was declined.", exception.Message);
|
||||||
|
|
||||||
@@ -425,8 +599,36 @@ public class StripePaymentServiceTests
|
|||||||
});
|
});
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
|
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
|
||||||
|
|
||||||
var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually);
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, 0, 0, false, taxInfo);
|
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plans, 0, 0, false, taxInfo);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void UpgradeFreeOrganizationAsync_SM_Success(SutProvider<StripePaymentService> sutProvider,
|
||||||
|
Organization organization, TaxInfo taxInfo)
|
||||||
|
{
|
||||||
|
organization.GatewaySubscriptionId = null;
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
||||||
|
{
|
||||||
|
Id = "C-1",
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "btCustomerId", "B-123" },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice
|
||||||
|
{
|
||||||
|
PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", },
|
||||||
|
AmountDue = 0
|
||||||
|
});
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
|
||||||
|
|
||||||
|
var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually).ToList();
|
||||||
|
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plans, 0, 0,
|
||||||
|
false, taxInfo);
|
||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(1)
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUserView]
|
||||||
|
WHERE
|
||||||
|
OrganizationId = @OrganizationId
|
||||||
|
AND Status >= 0 --Invited
|
||||||
|
AND AccessSecretsManager = 1
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[ServiceAccount_ReadCountByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(1)
|
||||||
|
FROM
|
||||||
|
[dbo].[ServiceAccount]
|
||||||
|
WHERE
|
||||||
|
OrganizationId = @OrganizationId
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
Reference in New Issue
Block a user