mirror of
https://github.com/bitwarden/server.git
synced 2025-07-04 09:32:48 -05:00
Merge branch 'master' into flexible-collections/deprecate-custom-collection-perm
This commit is contained in:
@ -30,29 +30,36 @@ public class MaxProjectsQueryTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
||||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually2019)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
|
||||||
[BitAutoData(PlanType.Custom)]
|
[BitAutoData(PlanType.Custom)]
|
||||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||||
public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
|
public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
|
||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||||
{
|
{
|
||||||
organization.PlanType = planType;
|
organization.PlanType = planType;
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BadRequestException>(
|
await Assert.ThrowsAsync<BadRequestException>(
|
||||||
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
|
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
|
await sutProvider.GetDependency<IProjectRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
[BitAutoData(PlanType.TeamsMonthly)]
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||||
[BitAutoData(PlanType.TeamsAnnually)]
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
||||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||||
|
@ -214,9 +214,12 @@ public class OrganizationsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(organization.GatewayCustomerId) && !string.IsNullOrWhiteSpace(organization.BillingEmail))
|
||||||
{
|
{
|
||||||
await _stripeSyncService.UpdateCustomerEmailAddress(organization.GatewayCustomerId, organization.BillingEmail);
|
await _stripeSyncService.UpdateCustomerEmailAddress(organization.GatewayCustomerId, organization.BillingEmail);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (StripeException stripeException)
|
catch (StripeException stripeException)
|
||||||
{
|
{
|
||||||
_logger.LogError(stripeException, "Failed to update billing email address in Stripe for Organization with ID '{organizationId}'", organization.Id);
|
_logger.LogError(stripeException, "Failed to update billing email address in Stripe for Organization with ID '{organizationId}'", organization.Id);
|
||||||
|
@ -259,7 +259,7 @@
|
|||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="ExpirationDate"></label>
|
<label asp-for="ExpirationDate"></label>
|
||||||
<input type="datetime-local" class="form-control" asp-for="ExpirationDate" readonly='@(!canEditLicensing)'>
|
<input type="datetime-local" class="form-control" asp-for="ExpirationDate" readonly='@(!canEditLicensing)' step="1">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,6 +68,10 @@
|
|||||||
|
|
||||||
function togglePlanFeatures(planType) {
|
function togglePlanFeatures(planType) {
|
||||||
switch(planType) {
|
switch(planType) {
|
||||||
|
case '@((byte)PlanType.TeamsMonthly2019)':
|
||||||
|
case '@((byte)PlanType.TeamsAnnually2019)':
|
||||||
|
case '@((byte)PlanType.TeamsMonthly2020)':
|
||||||
|
case '@((byte)PlanType.TeamsAnnually2020)':
|
||||||
case '@((byte)PlanType.TeamsMonthly)':
|
case '@((byte)PlanType.TeamsMonthly)':
|
||||||
case '@((byte)PlanType.TeamsAnnually)':
|
case '@((byte)PlanType.TeamsAnnually)':
|
||||||
document.getElementById('@(nameof(Model.UsePolicies))').checked = false;
|
document.getElementById('@(nameof(Model.UsePolicies))').checked = false;
|
||||||
@ -85,6 +89,10 @@
|
|||||||
document.getElementById('@(nameof(Model.UseScim))').checked = false;
|
document.getElementById('@(nameof(Model.UseScim))').checked = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case '@((byte)PlanType.EnterpriseMonthly2019)':
|
||||||
|
case '@((byte)PlanType.EnterpriseAnnually2019)':
|
||||||
|
case '@((byte)PlanType.EnterpriseMonthly2020)':
|
||||||
|
case '@((byte)PlanType.EnterpriseAnnually2020)':
|
||||||
case '@((byte)PlanType.EnterpriseMonthly)':
|
case '@((byte)PlanType.EnterpriseMonthly)':
|
||||||
case '@((byte)PlanType.EnterpriseAnnually)':
|
case '@((byte)PlanType.EnterpriseAnnually)':
|
||||||
document.getElementById('@(nameof(Model.UsePolicies))').checked = true;
|
document.getElementById('@(nameof(Model.UsePolicies))').checked = true;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Api.Auth.Models.Request;
|
using Bit.Api.AdminConsole.Models.Request;
|
||||||
using Bit.Api.Auth.Models.Response;
|
using Bit.Api.AdminConsole.Models.Response;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.OrganizationAuth.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationAuth.Interfaces;
|
||||||
|
@ -320,8 +320,16 @@ public class OrganizationsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _upgradeOrganizationPlanCommand.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
|
var (success, paymentIntentClientSecret) = await _upgradeOrganizationPlanCommand.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
|
||||||
return new PaymentResponseModel { Success = result.Item1, PaymentIntentClientSecret = result.Item2 };
|
|
||||||
|
if (model.UseSecretsManager && success)
|
||||||
|
{
|
||||||
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
|
await TryGrantOwnerAccessToSecretsManagerAsync(orgIdGuid, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PaymentResponseModel { Success = success, PaymentIntentClientSecret = paymentIntentClientSecret };
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/subscription")]
|
[HttpPost("{id}/subscription")]
|
||||||
@ -374,6 +382,9 @@ public class OrganizationsController : Controller
|
|||||||
model.AdditionalServiceAccounts);
|
model.AdditionalServiceAccounts);
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
|
await TryGrantOwnerAccessToSecretsManagerAsync(organization.Id, userId);
|
||||||
|
|
||||||
var organizationDetails = await _organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id,
|
var organizationDetails = await _organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id,
|
||||||
OrganizationUserStatusType.Confirmed);
|
OrganizationUserStatusType.Confirmed);
|
||||||
|
|
||||||
@ -786,4 +797,15 @@ public class OrganizationsController : Controller
|
|||||||
await _organizationService.UpdateAsync(model.ToOrganization(organization));
|
await _organizationService.UpdateAsync(model.ToOrganization(organization));
|
||||||
return new OrganizationResponseModel(organization);
|
return new OrganizationResponseModel(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task TryGrantOwnerAccessToSecretsManagerAsync(Guid organizationId, Guid userId)
|
||||||
|
{
|
||||||
|
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
||||||
|
|
||||||
|
if (organizationUser != null)
|
||||||
|
{
|
||||||
|
organizationUser.AccessSecretsManager = true;
|
||||||
|
await _organizationUserRepository.ReplaceAsync(organizationUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.Auth.Models.Request;
|
namespace Bit.Api.AdminConsole.Models.Request;
|
||||||
|
|
||||||
public class AdminAuthRequestUpdateRequestModel
|
public class AdminAuthRequestUpdateRequestModel
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Api.Auth.Models.Request;
|
namespace Bit.Api.AdminConsole.Models.Request;
|
||||||
|
|
||||||
public class BulkDenyAdminAuthRequestRequestModel
|
public class BulkDenyAdminAuthRequestRequestModel
|
||||||
{
|
{
|
@ -3,7 +3,7 @@ using System.Reflection;
|
|||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
namespace Bit.Api.Auth.Models.Response;
|
namespace Bit.Api.AdminConsole.Models.Response;
|
||||||
|
|
||||||
public class PendingOrganizationAuthRequestResponseModel : ResponseModel
|
public class PendingOrganizationAuthRequestResponseModel : ResponseModel
|
||||||
{
|
{
|
@ -32,8 +32,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
UsePasswordManager = organization.UsePasswordManager;
|
UsePasswordManager = organization.UsePasswordManager;
|
||||||
UsersGetPremium = organization.UsersGetPremium;
|
UsersGetPremium = organization.UsersGetPremium;
|
||||||
UseCustomPermissions = organization.UseCustomPermissions;
|
UseCustomPermissions = organization.UseCustomPermissions;
|
||||||
UseActivateAutofillPolicy = organization.PlanType == PlanType.EnterpriseAnnually ||
|
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).Product == ProductType.Enterprise;
|
||||||
organization.PlanType == PlanType.EnterpriseMonthly;
|
|
||||||
SelfHost = organization.SelfHost;
|
SelfHost = organization.SelfHost;
|
||||||
Seats = organization.Seats;
|
Seats = organization.Seats;
|
||||||
MaxCollections = organization.MaxCollections;
|
MaxCollections = organization.MaxCollections;
|
||||||
|
@ -25,8 +25,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
|||||||
UseResetPassword = organization.UseResetPassword;
|
UseResetPassword = organization.UseResetPassword;
|
||||||
UsersGetPremium = organization.UsersGetPremium;
|
UsersGetPremium = organization.UsersGetPremium;
|
||||||
UseCustomPermissions = organization.UseCustomPermissions;
|
UseCustomPermissions = organization.UseCustomPermissions;
|
||||||
UseActivateAutofillPolicy = organization.PlanType == PlanType.EnterpriseAnnually ||
|
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).Product == ProductType.Enterprise;
|
||||||
organization.PlanType == PlanType.EnterpriseMonthly;
|
|
||||||
SelfHost = organization.SelfHost;
|
SelfHost = organization.SelfHost;
|
||||||
Seats = organization.Seats;
|
Seats = organization.Seats;
|
||||||
MaxCollections = organization.MaxCollections;
|
MaxCollections = organization.MaxCollections;
|
||||||
|
@ -5,6 +5,7 @@ using Bit.Api.Models.Response;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
@ -22,15 +23,18 @@ public class WebAuthnController : Controller
|
|||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
||||||
private readonly IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector;
|
private readonly IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector;
|
||||||
|
private readonly IPolicyService _policyService;
|
||||||
|
|
||||||
public WebAuthnController(
|
public WebAuthnController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IWebAuthnCredentialRepository credentialRepository,
|
IWebAuthnCredentialRepository credentialRepository,
|
||||||
IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> createOptionsDataProtector)
|
IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> createOptionsDataProtector,
|
||||||
|
IPolicyService policyService)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_credentialRepository = credentialRepository;
|
_credentialRepository = credentialRepository;
|
||||||
_createOptionsDataProtector = createOptionsDataProtector;
|
_createOptionsDataProtector = createOptionsDataProtector;
|
||||||
|
_policyService = policyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
@ -46,6 +50,7 @@ public class WebAuthnController : Controller
|
|||||||
public async Task<WebAuthnCredentialCreateOptionsResponseModel> PostOptions([FromBody] SecretVerificationRequestModel model)
|
public async Task<WebAuthnCredentialCreateOptionsResponseModel> PostOptions([FromBody] SecretVerificationRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await VerifyUserAsync(model);
|
var user = await VerifyUserAsync(model);
|
||||||
|
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||||
var options = await _userService.StartWebAuthnLoginRegistrationAsync(user);
|
var options = await _userService.StartWebAuthnLoginRegistrationAsync(user);
|
||||||
|
|
||||||
var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options);
|
var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options);
|
||||||
@ -62,7 +67,9 @@ public class WebAuthnController : Controller
|
|||||||
public async Task Post([FromBody] WebAuthnCredentialRequestModel model)
|
public async Task Post([FromBody] WebAuthnCredentialRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await GetUserAsync();
|
var user = await GetUserAsync();
|
||||||
|
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||||
var tokenable = _createOptionsDataProtector.Unprotect(model.Token);
|
var tokenable = _createOptionsDataProtector.Unprotect(model.Token);
|
||||||
|
|
||||||
if (!tokenable.TokenIsValid(user))
|
if (!tokenable.TokenIsValid(user))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("The token associated with your request is expired. A valid token is required to continue.");
|
throw new BadRequestException("The token associated with your request is expired. A valid token is required to continue.");
|
||||||
@ -75,6 +82,16 @@ public class WebAuthnController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ValidateRequireSsoPolicyDisabledOrNotApplicable(Guid userId)
|
||||||
|
{
|
||||||
|
var requireSsoLogin = await _policyService.AnyPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso);
|
||||||
|
|
||||||
|
if (requireSsoLogin)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Passkeys cannot be created for your account. SSO login is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/delete")]
|
[HttpPost("{id}/delete")]
|
||||||
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
|
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -11,17 +15,39 @@ namespace Bit.Api.Controllers;
|
|||||||
public class PlansController : Controller
|
public class PlansController : Controller
|
||||||
{
|
{
|
||||||
private readonly ITaxRateRepository _taxRateRepository;
|
private readonly ITaxRateRepository _taxRateRepository;
|
||||||
public PlansController(ITaxRateRepository taxRateRepository)
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
|
public PlansController(
|
||||||
|
ITaxRateRepository taxRateRepository,
|
||||||
|
IFeatureService featureService,
|
||||||
|
ICurrentContext currentContext)
|
||||||
{
|
{
|
||||||
_taxRateRepository = taxRateRepository;
|
_taxRateRepository = taxRateRepository;
|
||||||
|
_featureService = featureService;
|
||||||
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public ListResponseModel<PlanResponseModel> Get()
|
public ListResponseModel<PlanResponseModel> Get()
|
||||||
{
|
{
|
||||||
|
var plansUpgradeIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.BillingPlansUpgrade, _currentContext);
|
||||||
var data = StaticStore.Plans;
|
var data = StaticStore.Plans;
|
||||||
var responses = data.Select(plan => new PlanResponseModel(plan));
|
var responses = data
|
||||||
|
.Where(plan => plansUpgradeIsEnabled || plan.Type <= PlanType.EnterpriseAnnually2020)
|
||||||
|
.Select(plan =>
|
||||||
|
{
|
||||||
|
if (!plansUpgradeIsEnabled && plan.Type is <= PlanType.EnterpriseAnnually2020 and >= PlanType.TeamsMonthly2020)
|
||||||
|
{
|
||||||
|
plan.LegacyYear = null;
|
||||||
|
}
|
||||||
|
else if (plansUpgradeIsEnabled && plan.Type is <= PlanType.EnterpriseAnnually2020 and >= PlanType.TeamsMonthly2020)
|
||||||
|
{
|
||||||
|
plan.LegacyYear = 2023;
|
||||||
|
}
|
||||||
|
return new PlanResponseModel(plan);
|
||||||
|
});
|
||||||
return new ListResponseModel<PlanResponseModel>(responses);
|
return new ListResponseModel<PlanResponseModel>(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,14 +159,18 @@ public class FreshsalesController : Controller
|
|||||||
planName = "Families";
|
planName = "Families";
|
||||||
return true;
|
return true;
|
||||||
case PlanType.TeamsAnnually:
|
case PlanType.TeamsAnnually:
|
||||||
|
case PlanType.TeamsAnnually2020:
|
||||||
case PlanType.TeamsAnnually2019:
|
case PlanType.TeamsAnnually2019:
|
||||||
case PlanType.TeamsMonthly:
|
case PlanType.TeamsMonthly:
|
||||||
|
case PlanType.TeamsMonthly2020:
|
||||||
case PlanType.TeamsMonthly2019:
|
case PlanType.TeamsMonthly2019:
|
||||||
planName = "Teams";
|
planName = "Teams";
|
||||||
return true;
|
return true;
|
||||||
case PlanType.EnterpriseAnnually:
|
case PlanType.EnterpriseAnnually:
|
||||||
|
case PlanType.EnterpriseAnnually2020:
|
||||||
case PlanType.EnterpriseAnnually2019:
|
case PlanType.EnterpriseAnnually2019:
|
||||||
case PlanType.EnterpriseMonthly:
|
case PlanType.EnterpriseMonthly:
|
||||||
|
case PlanType.EnterpriseMonthly2020:
|
||||||
case PlanType.EnterpriseMonthly2019:
|
case PlanType.EnterpriseMonthly2019:
|
||||||
planName = "Enterprise";
|
planName = "Enterprise";
|
||||||
return true;
|
return true;
|
||||||
|
@ -668,18 +668,7 @@ public class StripeController : Controller
|
|||||||
return new Tuple<Guid?, Guid?>(orgId, userId);
|
return new Tuple<Guid?, Guid?>(orgId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool OrgPlanForInvoiceNotifications(Organization org)
|
private static bool OrgPlanForInvoiceNotifications(Organization org) => StaticStore.GetPlan(org.PlanType).IsAnnual;
|
||||||
{
|
|
||||||
switch (org.PlanType)
|
|
||||||
{
|
|
||||||
case PlanType.FamiliesAnnually:
|
|
||||||
case PlanType.TeamsAnnually:
|
|
||||||
case PlanType.EnterpriseAnnually:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false)
|
private async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false)
|
||||||
{
|
{
|
||||||
|
@ -15,9 +15,9 @@ public class PayPalIpnClient
|
|||||||
public PayPalIpnClient(IOptions<BillingSettings> billingSettings, ILogger<PayPalIpnClient> logger)
|
public PayPalIpnClient(IOptions<BillingSettings> billingSettings, ILogger<PayPalIpnClient> logger)
|
||||||
{
|
{
|
||||||
var bSettings = billingSettings?.Value;
|
var bSettings = billingSettings?.Value;
|
||||||
_ipnUri = new Uri(bSettings.PayPal.Production ? "https://ipnpb.paypal.com/cgi-bin/webscr" :
|
|
||||||
"https://ipnpb.sandbox.paypal.com/cgi-bin/webscr");
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_ipnUri = new Uri(bSettings.PayPal.Production ? "https://www.paypal.com/cgi-bin/webscr" :
|
||||||
|
"https://www.sandbox.paypal.com/cgi-bin/webscr");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> VerifyIpnAsync(string ipnBody)
|
public async Task<bool> VerifyIpnAsync(string ipnBody)
|
||||||
@ -29,19 +29,16 @@ public class PayPalIpnClient
|
|||||||
throw new ArgumentException("No IPN body.");
|
throw new ArgumentException("No IPN body.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = new HttpRequestMessage
|
var request = new HttpRequestMessage { Method = HttpMethod.Post, RequestUri = _ipnUri };
|
||||||
{
|
|
||||||
Method = HttpMethod.Post,
|
|
||||||
RequestUri = _ipnUri
|
|
||||||
};
|
|
||||||
_httpClient.DefaultRequestHeaders.Add("User-Agent", "CSharp-IPN-VerificationScript");
|
|
||||||
var cmdIpnBody = string.Concat("cmd=_notify-validate&", ipnBody);
|
var cmdIpnBody = string.Concat("cmd=_notify-validate&", ipnBody);
|
||||||
request.Content = new StringContent(cmdIpnBody, Encoding.UTF8, "application/x-www-form-urlencoded");
|
request.Content = new StringContent(cmdIpnBody, Encoding.UTF8, "application/x-www-form-urlencoded");
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await _httpClient.SendAsync(request);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Failed to receive a successful response from PayPal IPN verification service. Response: {Response}", response);
|
||||||
throw new Exception("Failed to verify IPN, status: " + response.StatusCode);
|
throw new Exception("Failed to verify IPN, status: " + response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
if (responseContent.Equals("VERIFIED"))
|
if (responseContent.Equals("VERIFIED"))
|
||||||
{
|
{
|
||||||
@ -53,6 +50,7 @@ public class PayPalIpnClient
|
|||||||
_logger.LogWarning("Received an INVALID response from PayPal: {ResponseContent}", responseContent);
|
_logger.LogWarning("Received an INVALID response from PayPal: {ResponseContent}", responseContent);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogError("Failed to verify IPN: {ResponseContent}", responseContent);
|
_logger.LogError("Failed to verify IPN: {ResponseContent}", responseContent);
|
||||||
throw new Exception("Failed to verify IPN.");
|
throw new Exception("Failed to verify IPN.");
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,20 @@ public static class Constants
|
|||||||
public const string Fido2KeyCipherMinimumVersion = "2023.10.0";
|
public const string Fido2KeyCipherMinimumVersion = "2023.10.0";
|
||||||
|
|
||||||
public const string CipherKeyEncryptionMinimumVersion = "2023.9.2";
|
public const string CipherKeyEncryptionMinimumVersion = "2023.9.2";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When you set the ProrationBehavior to create_prorations,
|
||||||
|
/// Stripe will automatically create prorations for any changes made to the subscription,
|
||||||
|
/// such as changing the plan, adding or removing quantities, or applying discounts.
|
||||||
|
/// </summary>
|
||||||
|
public const string CreateProrations = "create_prorations";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When you set the ProrationBehavior to always_invoice,
|
||||||
|
/// Stripe will always generate an invoice when a subscription update occurs,
|
||||||
|
/// regardless of whether there is a proration or not.
|
||||||
|
/// </summary>
|
||||||
|
public const string AlwaysInvoice = "always_invoice";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class TokenPurposes
|
public static class TokenPurposes
|
||||||
@ -49,6 +63,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string BulkCollectionAccess = "bulk-collection-access";
|
public const string BulkCollectionAccess = "bulk-collection-access";
|
||||||
public const string AutofillOverlay = "autofill-overlay";
|
public const string AutofillOverlay = "autofill-overlay";
|
||||||
public const string ItemShare = "item-share";
|
public const string ItemShare = "item-share";
|
||||||
|
public const string BillingPlansUpgrade = "billing-plans-upgrade";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -20,12 +20,20 @@ public enum PlanType : byte
|
|||||||
Custom = 6,
|
Custom = 6,
|
||||||
[Display(Name = "Families")]
|
[Display(Name = "Families")]
|
||||||
FamiliesAnnually = 7,
|
FamiliesAnnually = 7,
|
||||||
|
[Display(Name = "Teams (Monthly) 2020")]
|
||||||
|
TeamsMonthly2020 = 8,
|
||||||
|
[Display(Name = "Teams (Annually) 2020")]
|
||||||
|
TeamsAnnually2020 = 9,
|
||||||
|
[Display(Name = "Enterprise (Monthly) 2020")]
|
||||||
|
EnterpriseMonthly2020 = 10,
|
||||||
|
[Display(Name = "Enterprise (Annually) 2020")]
|
||||||
|
EnterpriseAnnually2020 = 11,
|
||||||
[Display(Name = "Teams (Monthly)")]
|
[Display(Name = "Teams (Monthly)")]
|
||||||
TeamsMonthly = 8,
|
TeamsMonthly = 12,
|
||||||
[Display(Name = "Teams (Annually)")]
|
[Display(Name = "Teams (Annually)")]
|
||||||
TeamsAnnually = 9,
|
TeamsAnnually = 13,
|
||||||
[Display(Name = "Enterprise (Monthly)")]
|
[Display(Name = "Enterprise (Monthly)")]
|
||||||
EnterpriseMonthly = 10,
|
EnterpriseMonthly = 14,
|
||||||
[Display(Name = "Enterprise (Annually)")]
|
[Display(Name = "Enterprise (Annually)")]
|
||||||
EnterpriseAnnually = 11,
|
EnterpriseAnnually = 15,
|
||||||
}
|
}
|
||||||
|
7
src/Core/Models/Business/InvoicePreviewResult.cs
Normal file
7
src/Core/Models/Business/InvoicePreviewResult.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Core.Models.Business;
|
||||||
|
|
||||||
|
public class InvoicePreviewResult
|
||||||
|
{
|
||||||
|
public bool IsInvoicedNow { get; set; }
|
||||||
|
public string PaymentIntentClientSecret { get; set; }
|
||||||
|
}
|
@ -184,8 +184,11 @@ public class OrganizationLicense : ILicense
|
|||||||
(Version >= 11 || !p.Name.Equals(nameof(UseCustomPermissions))) &&
|
(Version >= 11 || !p.Name.Equals(nameof(UseCustomPermissions))) &&
|
||||||
// ExpirationWithoutGracePeriod was added in Version 12
|
// ExpirationWithoutGracePeriod was added in Version 12
|
||||||
(Version >= 12 || !p.Name.Equals(nameof(ExpirationWithoutGracePeriod))) &&
|
(Version >= 12 || !p.Name.Equals(nameof(ExpirationWithoutGracePeriod))) &&
|
||||||
// UseSecretsManager was added in Version 13
|
// UseSecretsManager, UsePasswordManager, SmSeats, and SmServiceAccounts were added in Version 13
|
||||||
(Version >= 13 || !p.Name.Equals(nameof(UseSecretsManager))) &&
|
(Version >= 13 || !p.Name.Equals(nameof(UseSecretsManager))) &&
|
||||||
|
(Version >= 13 || !p.Name.Equals(nameof(UsePasswordManager))) &&
|
||||||
|
(Version >= 13 || !p.Name.Equals(nameof(SmSeats))) &&
|
||||||
|
(Version >= 13 || !p.Name.Equals(nameof(SmServiceAccounts))) &&
|
||||||
(
|
(
|
||||||
!forHash ||
|
!forHash ||
|
||||||
(
|
(
|
||||||
|
9
src/Core/Models/Business/PendingInoviceItems.cs
Normal file
9
src/Core/Models/Business/PendingInoviceItems.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Business;
|
||||||
|
|
||||||
|
public class PendingInoviceItems
|
||||||
|
{
|
||||||
|
public IEnumerable<InvoiceItem> PendingInvoiceItems { get; set; }
|
||||||
|
public IDictionary<string, InvoiceItem> PendingInvoiceItemsDict { get; set; }
|
||||||
|
}
|
@ -44,7 +44,7 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate
|
|||||||
{
|
{
|
||||||
updatedItems.Add(new SubscriptionItemOptions
|
updatedItems.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = _plan.SecretsManager.StripeSeatPlanId,
|
Plan = _plan.SecretsManager.StripeSeatPlanId,
|
||||||
Quantity = _additionalSeats
|
Quantity = _additionalSeats
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate
|
|||||||
{
|
{
|
||||||
updatedItems.Add(new SubscriptionItemOptions
|
updatedItems.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = _plan.SecretsManager.StripeServiceAccountPlanId,
|
Plan = _plan.SecretsManager.StripeServiceAccountPlanId,
|
||||||
Quantity = _additionalServiceAccounts
|
Quantity = _additionalServiceAccounts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -63,14 +63,14 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate
|
|||||||
{
|
{
|
||||||
updatedItems.Add(new SubscriptionItemOptions
|
updatedItems.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = _plan.SecretsManager.StripeSeatPlanId,
|
Plan = _plan.SecretsManager.StripeSeatPlanId,
|
||||||
Quantity = _previousSeats,
|
Quantity = _previousSeats,
|
||||||
Deleted = _previousSeats == 0 ? true : (bool?)null,
|
Deleted = _previousSeats == 0 ? true : (bool?)null,
|
||||||
});
|
});
|
||||||
|
|
||||||
updatedItems.Add(new SubscriptionItemOptions
|
updatedItems.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Price = _plan.SecretsManager.StripeServiceAccountPlanId,
|
Plan = _plan.SecretsManager.StripeServiceAccountPlanId,
|
||||||
Quantity = _previousServiceAccounts,
|
Quantity = _previousServiceAccounts,
|
||||||
Deleted = _previousServiceAccounts == 0 ? true : (bool?)null,
|
Deleted = _previousServiceAccounts == 0 ? true : (bool?)null,
|
||||||
});
|
});
|
||||||
|
@ -28,7 +28,7 @@ public abstract record Plan
|
|||||||
public bool HasCustomPermissions { get; protected init; }
|
public bool HasCustomPermissions { get; protected init; }
|
||||||
public int UpgradeSortOrder { get; protected init; }
|
public int UpgradeSortOrder { get; protected init; }
|
||||||
public int DisplaySortOrder { get; protected init; }
|
public int DisplaySortOrder { get; protected init; }
|
||||||
public int? LegacyYear { get; protected init; }
|
public int? LegacyYear { get; set; }
|
||||||
public bool Disabled { get; protected init; }
|
public bool Disabled { get; protected init; }
|
||||||
public PasswordManagerPlanFeatures PasswordManager { get; protected init; }
|
public PasswordManagerPlanFeatures PasswordManager { get; protected init; }
|
||||||
public SecretsManagerPlanFeatures SecretsManager { get; protected init; }
|
public SecretsManagerPlanFeatures SecretsManager { get; protected init; }
|
||||||
|
@ -24,6 +24,10 @@ public record Enterprise2019Plan : Models.StaticStore.Plan
|
|||||||
HasTotp = true;
|
HasTotp = true;
|
||||||
Has2fa = true;
|
Has2fa = true;
|
||||||
HasApi = true;
|
HasApi = true;
|
||||||
|
HasSso = true;
|
||||||
|
HasKeyConnector = true;
|
||||||
|
HasScim = true;
|
||||||
|
HasResetPassword = true;
|
||||||
UsersGetPremium = true;
|
UsersGetPremium = true;
|
||||||
HasCustomPermissions = true;
|
HasCustomPermissions = true;
|
||||||
|
|
||||||
@ -31,9 +35,41 @@ public record Enterprise2019Plan : Models.StaticStore.Plan
|
|||||||
DisplaySortOrder = 3;
|
DisplaySortOrder = 3;
|
||||||
LegacyYear = 2020;
|
LegacyYear = 2020;
|
||||||
|
|
||||||
|
SecretsManager = new Enterprise2019SecretsManagerFeatures(isAnnual);
|
||||||
PasswordManager = new Enterprise2019PasswordManagerFeatures(isAnnual);
|
PasswordManager = new Enterprise2019PasswordManagerFeatures(isAnnual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record Enterprise2019SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public Enterprise2019SecretsManagerFeatures(bool isAnnual)
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BasePrice = 0;
|
||||||
|
BaseServiceAccount = 200;
|
||||||
|
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
HasAdditionalServiceAccountOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
AllowServiceAccountsAutoscale = true;
|
||||||
|
|
||||||
|
if (isAnnual)
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||||
|
SeatPrice = 144;
|
||||||
|
AdditionalPricePerServiceAccount = 6;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||||
|
SeatPrice = 13;
|
||||||
|
AdditionalPricePerServiceAccount = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record Enterprise2019PasswordManagerFeatures : PasswordManagerPlanFeatures
|
private record Enterprise2019PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||||
{
|
{
|
||||||
public Enterprise2019PasswordManagerFeatures(bool isAnnual)
|
public Enterprise2019PasswordManagerFeatures(bool isAnnual)
|
||||||
|
101
src/Core/Models/StaticStore/Plans/Enterprise2020Plan.cs
Normal file
101
src/Core/Models/StaticStore/Plans/Enterprise2020Plan.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.StaticStore.Plans;
|
||||||
|
|
||||||
|
public record Enterprise2020Plan : Models.StaticStore.Plan
|
||||||
|
{
|
||||||
|
public Enterprise2020Plan(bool isAnnual)
|
||||||
|
{
|
||||||
|
Type = isAnnual ? PlanType.EnterpriseAnnually2020 : PlanType.EnterpriseMonthly2020;
|
||||||
|
Product = ProductType.Enterprise;
|
||||||
|
Name = isAnnual ? "Enterprise (Annually) 2020" : "Enterprise (Monthly) 2020";
|
||||||
|
IsAnnual = isAnnual;
|
||||||
|
NameLocalizationKey = "planNameEnterprise";
|
||||||
|
DescriptionLocalizationKey = "planDescEnterprise";
|
||||||
|
CanBeUsedByBusiness = true;
|
||||||
|
|
||||||
|
TrialPeriodDays = 7;
|
||||||
|
|
||||||
|
HasPolicies = true;
|
||||||
|
HasSelfHost = true;
|
||||||
|
HasGroups = true;
|
||||||
|
HasDirectory = true;
|
||||||
|
HasEvents = true;
|
||||||
|
HasTotp = true;
|
||||||
|
Has2fa = true;
|
||||||
|
HasApi = true;
|
||||||
|
HasSso = true;
|
||||||
|
HasKeyConnector = true;
|
||||||
|
HasScim = true;
|
||||||
|
HasResetPassword = true;
|
||||||
|
UsersGetPremium = true;
|
||||||
|
HasCustomPermissions = true;
|
||||||
|
|
||||||
|
UpgradeSortOrder = 3;
|
||||||
|
DisplaySortOrder = 3;
|
||||||
|
LegacyYear = 2023;
|
||||||
|
|
||||||
|
PasswordManager = new Enterprise2020PasswordManagerFeatures(isAnnual);
|
||||||
|
SecretsManager = new Enterprise2020SecretsManagerFeatures(isAnnual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Enterprise2020SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public Enterprise2020SecretsManagerFeatures(bool isAnnual)
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BasePrice = 0;
|
||||||
|
BaseServiceAccount = 200;
|
||||||
|
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
HasAdditionalServiceAccountOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
AllowServiceAccountsAutoscale = true;
|
||||||
|
|
||||||
|
if (isAnnual)
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||||
|
SeatPrice = 144;
|
||||||
|
AdditionalPricePerServiceAccount = 6;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||||
|
SeatPrice = 13;
|
||||||
|
AdditionalPricePerServiceAccount = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Enterprise2020PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public Enterprise2020PasswordManagerFeatures(bool isAnnual)
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BaseStorageGb = 1;
|
||||||
|
|
||||||
|
HasAdditionalStorageOption = true;
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
|
||||||
|
if (isAnnual)
|
||||||
|
{
|
||||||
|
AdditionalStoragePricePerGb = 4;
|
||||||
|
StripeStoragePlanId = "storage-gb-annually";
|
||||||
|
StripeSeatPlanId = "2020-enterprise-org-seat-annually";
|
||||||
|
SeatPrice = 60;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "2020-enterprise-seat-monthly";
|
||||||
|
StripeStoragePlanId = "storage-gb-monthly";
|
||||||
|
SeatPrice = 6;
|
||||||
|
AdditionalStoragePricePerGb = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -85,14 +85,14 @@ public record EnterprisePlan : Models.StaticStore.Plan
|
|||||||
{
|
{
|
||||||
AdditionalStoragePricePerGb = 4;
|
AdditionalStoragePricePerGb = 4;
|
||||||
StripeStoragePlanId = "storage-gb-annually";
|
StripeStoragePlanId = "storage-gb-annually";
|
||||||
StripeSeatPlanId = "2020-enterprise-org-seat-annually";
|
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
|
||||||
SeatPrice = 60;
|
SeatPrice = 72;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
StripeSeatPlanId = "2020-enterprise-seat-monthly";
|
StripeSeatPlanId = "2023-enterprise-seat-monthly";
|
||||||
StripeStoragePlanId = "storage-gb-monthly";
|
StripeStoragePlanId = "storage-gb-monthly";
|
||||||
SeatPrice = 6;
|
SeatPrice = 7;
|
||||||
AdditionalStoragePricePerGb = 0.5M;
|
AdditionalStoragePricePerGb = 0.5M;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ public record Families2019Plan : Models.StaticStore.Plan
|
|||||||
|
|
||||||
HasSelfHost = true;
|
HasSelfHost = true;
|
||||||
HasTotp = true;
|
HasTotp = true;
|
||||||
|
UsersGetPremium = true;
|
||||||
|
|
||||||
UpgradeSortOrder = 1;
|
UpgradeSortOrder = 1;
|
||||||
DisplaySortOrder = 1;
|
DisplaySortOrder = 1;
|
||||||
|
@ -16,15 +16,53 @@ public record Teams2019Plan : Models.StaticStore.Plan
|
|||||||
|
|
||||||
TrialPeriodDays = 7;
|
TrialPeriodDays = 7;
|
||||||
|
|
||||||
|
HasGroups = true;
|
||||||
|
HasDirectory = true;
|
||||||
|
HasEvents = true;
|
||||||
HasTotp = true;
|
HasTotp = true;
|
||||||
|
Has2fa = true;
|
||||||
|
HasApi = true;
|
||||||
|
UsersGetPremium = true;
|
||||||
|
|
||||||
UpgradeSortOrder = 2;
|
UpgradeSortOrder = 2;
|
||||||
DisplaySortOrder = 2;
|
DisplaySortOrder = 2;
|
||||||
LegacyYear = 2020;
|
LegacyYear = 2020;
|
||||||
|
|
||||||
|
SecretsManager = new Teams2019SecretsManagerFeatures(isAnnual);
|
||||||
PasswordManager = new Teams2019PasswordManagerFeatures(isAnnual);
|
PasswordManager = new Teams2019PasswordManagerFeatures(isAnnual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record Teams2019SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public Teams2019SecretsManagerFeatures(bool isAnnual)
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BasePrice = 0;
|
||||||
|
BaseServiceAccount = 50;
|
||||||
|
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
HasAdditionalServiceAccountOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
AllowServiceAccountsAutoscale = true;
|
||||||
|
|
||||||
|
if (isAnnual)
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||||
|
SeatPrice = 72;
|
||||||
|
AdditionalPricePerServiceAccount = 6;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||||
|
SeatPrice = 7;
|
||||||
|
AdditionalPricePerServiceAccount = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record Teams2019PasswordManagerFeatures : PasswordManagerPlanFeatures
|
private record Teams2019PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||||
{
|
{
|
||||||
public Teams2019PasswordManagerFeatures(bool isAnnual)
|
public Teams2019PasswordManagerFeatures(bool isAnnual)
|
||||||
|
95
src/Core/Models/StaticStore/Plans/Teams2020Plan.cs
Normal file
95
src/Core/Models/StaticStore/Plans/Teams2020Plan.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.StaticStore.Plans;
|
||||||
|
|
||||||
|
public record Teams2020Plan : Models.StaticStore.Plan
|
||||||
|
{
|
||||||
|
public Teams2020Plan(bool isAnnual)
|
||||||
|
{
|
||||||
|
Type = isAnnual ? PlanType.TeamsAnnually2020 : PlanType.TeamsMonthly2020;
|
||||||
|
Product = ProductType.Teams;
|
||||||
|
Name = isAnnual ? "Teams (Annually) 2020" : "Teams (Monthly) 2020";
|
||||||
|
IsAnnual = isAnnual;
|
||||||
|
NameLocalizationKey = "planNameTeams";
|
||||||
|
DescriptionLocalizationKey = "planDescTeams";
|
||||||
|
CanBeUsedByBusiness = true;
|
||||||
|
|
||||||
|
TrialPeriodDays = 7;
|
||||||
|
|
||||||
|
HasGroups = true;
|
||||||
|
HasDirectory = true;
|
||||||
|
HasEvents = true;
|
||||||
|
HasTotp = true;
|
||||||
|
Has2fa = true;
|
||||||
|
HasApi = true;
|
||||||
|
UsersGetPremium = true;
|
||||||
|
|
||||||
|
UpgradeSortOrder = 2;
|
||||||
|
DisplaySortOrder = 2;
|
||||||
|
LegacyYear = 2023;
|
||||||
|
|
||||||
|
PasswordManager = new Teams2020PasswordManagerFeatures(isAnnual);
|
||||||
|
SecretsManager = new Teams2020SecretsManagerFeatures(isAnnual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Teams2020SecretsManagerFeatures : SecretsManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public Teams2020SecretsManagerFeatures(bool isAnnual)
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BasePrice = 0;
|
||||||
|
BaseServiceAccount = 50;
|
||||||
|
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
HasAdditionalServiceAccountOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
AllowServiceAccountsAutoscale = true;
|
||||||
|
|
||||||
|
if (isAnnual)
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
|
||||||
|
SeatPrice = 72;
|
||||||
|
AdditionalPricePerServiceAccount = 6;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
|
||||||
|
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
|
||||||
|
SeatPrice = 7;
|
||||||
|
AdditionalPricePerServiceAccount = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Teams2020PasswordManagerFeatures : PasswordManagerPlanFeatures
|
||||||
|
{
|
||||||
|
public Teams2020PasswordManagerFeatures(bool isAnnual)
|
||||||
|
{
|
||||||
|
BaseSeats = 0;
|
||||||
|
BaseStorageGb = 1;
|
||||||
|
BasePrice = 0;
|
||||||
|
|
||||||
|
HasAdditionalStorageOption = true;
|
||||||
|
HasAdditionalSeatsOption = true;
|
||||||
|
|
||||||
|
AllowSeatAutoscale = true;
|
||||||
|
|
||||||
|
if (isAnnual)
|
||||||
|
{
|
||||||
|
StripeStoragePlanId = "storage-gb-annually";
|
||||||
|
StripeSeatPlanId = "2020-teams-org-seat-annually";
|
||||||
|
SeatPrice = 36;
|
||||||
|
AdditionalStoragePricePerGb = 4;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StripeSeatPlanId = "2020-teams-org-seat-monthly";
|
||||||
|
StripeStoragePlanId = "storage-gb-monthly";
|
||||||
|
SeatPrice = 4;
|
||||||
|
AdditionalStoragePricePerGb = 0.5M;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,15 +78,15 @@ public record TeamsPlan : Models.StaticStore.Plan
|
|||||||
if (isAnnual)
|
if (isAnnual)
|
||||||
{
|
{
|
||||||
StripeStoragePlanId = "storage-gb-annually";
|
StripeStoragePlanId = "storage-gb-annually";
|
||||||
StripeSeatPlanId = "2020-teams-org-seat-annually";
|
StripeSeatPlanId = "2023-teams-org-seat-annually";
|
||||||
SeatPrice = 36;
|
SeatPrice = 48;
|
||||||
AdditionalStoragePricePerGb = 4;
|
AdditionalStoragePricePerGb = 4;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
StripeSeatPlanId = "2020-teams-org-seat-monthly";
|
StripeSeatPlanId = "2023-teams-org-seat-monthly";
|
||||||
StripeStoragePlanId = "storage-gb-monthly";
|
StripeStoragePlanId = "storage-gb-monthly";
|
||||||
SeatPrice = 4;
|
SeatPrice = 5;
|
||||||
AdditionalStoragePricePerGb = 0.5M;
|
AdditionalStoragePricePerGb = 0.5M;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Models.BitStripe;
|
using Bit.Core.Models.BitStripe;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
@ -14,8 +15,11 @@ public interface IStripeAdapter
|
|||||||
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
||||||
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
||||||
Task<Stripe.Invoice> InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options);
|
Task<Stripe.Invoice> InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options);
|
||||||
|
Task<Stripe.Invoice> InvoiceCreateAsync(Stripe.InvoiceCreateOptions options);
|
||||||
|
Task<Stripe.InvoiceItem> InvoiceItemCreateAsync(Stripe.InvoiceItemCreateOptions options);
|
||||||
Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options);
|
Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options);
|
||||||
Task<Stripe.StripeList<Stripe.Invoice>> InvoiceListAsync(Stripe.InvoiceListOptions options);
|
Task<Stripe.StripeList<Stripe.Invoice>> InvoiceListAsync(Stripe.InvoiceListOptions options);
|
||||||
|
IEnumerable<InvoiceItem> InvoiceItemListAsync(InvoiceItemListOptions options);
|
||||||
Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options);
|
Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options);
|
||||||
Task<Stripe.Invoice> InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options);
|
Task<Stripe.Invoice> InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options);
|
||||||
Task<Stripe.Invoice> InvoiceSendInvoiceAsync(string id, Stripe.InvoiceSendOptions options);
|
Task<Stripe.Invoice> InvoiceSendInvoiceAsync(string id, Stripe.InvoiceSendOptions options);
|
||||||
|
@ -98,14 +98,6 @@ public class PolicyService : IPolicyService
|
|||||||
await DependsOnSingleOrgAsync(org);
|
await DependsOnSingleOrgAsync(org);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Activate Autofill is only available to Enterprise 2020-current plans
|
|
||||||
case PolicyType.ActivateAutofill:
|
|
||||||
if (policy.Enabled)
|
|
||||||
{
|
|
||||||
LockedTo2020Plan(org);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
@ -274,14 +266,6 @@ public class PolicyService : IPolicyService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LockedTo2020Plan(Organization org)
|
|
||||||
{
|
|
||||||
if (org.PlanType != PlanType.EnterpriseAnnually && org.PlanType != PlanType.EnterpriseMonthly)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("This policy is only available to 2020 Enterprise plans.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RequiredBySsoTrustedDeviceEncryptionAsync(Organization org)
|
private async Task RequiredBySsoTrustedDeviceEncryptionAsync(Organization org)
|
||||||
{
|
{
|
||||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
|
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Models.BitStripe;
|
using Bit.Core.Models.BitStripe;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
private readonly Stripe.BankAccountService _bankAccountService;
|
private readonly Stripe.BankAccountService _bankAccountService;
|
||||||
private readonly Stripe.PriceService _priceService;
|
private readonly Stripe.PriceService _priceService;
|
||||||
private readonly Stripe.TestHelpers.TestClockService _testClockService;
|
private readonly Stripe.TestHelpers.TestClockService _testClockService;
|
||||||
|
private readonly Stripe.InvoiceItemService _invoiceItemService;
|
||||||
|
|
||||||
public StripeAdapter()
|
public StripeAdapter()
|
||||||
{
|
{
|
||||||
@ -31,6 +33,7 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
_bankAccountService = new Stripe.BankAccountService();
|
_bankAccountService = new Stripe.BankAccountService();
|
||||||
_priceService = new Stripe.PriceService();
|
_priceService = new Stripe.PriceService();
|
||||||
_testClockService = new Stripe.TestHelpers.TestClockService();
|
_testClockService = new Stripe.TestHelpers.TestClockService();
|
||||||
|
_invoiceItemService = new Stripe.InvoiceItemService();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options)
|
public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options)
|
||||||
@ -79,6 +82,16 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
return _invoiceService.UpcomingAsync(options);
|
return _invoiceService.UpcomingAsync(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<Stripe.Invoice> InvoiceCreateAsync(Stripe.InvoiceCreateOptions options)
|
||||||
|
{
|
||||||
|
return _invoiceService.CreateAsync(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Stripe.InvoiceItem> InvoiceItemCreateAsync(Stripe.InvoiceItemCreateOptions options)
|
||||||
|
{
|
||||||
|
return _invoiceItemService.CreateAsync(options);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options)
|
public Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options)
|
||||||
{
|
{
|
||||||
return _invoiceService.GetAsync(id, options);
|
return _invoiceService.GetAsync(id, options);
|
||||||
@ -89,6 +102,11 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
return _invoiceService.ListAsync(options);
|
return _invoiceService.ListAsync(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<InvoiceItem> InvoiceItemListAsync(InvoiceItemListOptions options)
|
||||||
|
{
|
||||||
|
return _invoiceItemService.ListAutoPaging(options);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options)
|
public Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options)
|
||||||
{
|
{
|
||||||
return _invoiceService.UpdateAsync(id, options);
|
return _invoiceService.UpdateAsync(id, options);
|
||||||
|
@ -6,6 +6,7 @@ using Bit.Core.Models.Business;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Stripe;
|
||||||
using StaticStore = Bit.Core.Models.StaticStore;
|
using StaticStore = Bit.Core.Models.StaticStore;
|
||||||
using TaxRate = Bit.Core.Entities.TaxRate;
|
using TaxRate = Bit.Core.Entities.TaxRate;
|
||||||
|
|
||||||
@ -749,16 +750,14 @@ public class StripePaymentService : IPaymentService
|
|||||||
prorationDate ??= DateTime.UtcNow;
|
prorationDate ??= DateTime.UtcNow;
|
||||||
var collectionMethod = sub.CollectionMethod;
|
var collectionMethod = sub.CollectionMethod;
|
||||||
var daysUntilDue = sub.DaysUntilDue;
|
var daysUntilDue = sub.DaysUntilDue;
|
||||||
var chargeNow = collectionMethod == "charge_automatically";
|
|
||||||
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
||||||
|
|
||||||
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions
|
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
Items = updatedItemOptions,
|
Items = updatedItemOptions,
|
||||||
ProrationBehavior = "always_invoice",
|
ProrationBehavior = Constants.CreateProrations,
|
||||||
DaysUntilDue = daysUntilDue ?? 1,
|
DaysUntilDue = daysUntilDue ?? 1,
|
||||||
CollectionMethod = "send_invoice",
|
CollectionMethod = "send_invoice"
|
||||||
ProrationDate = prorationDate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!subscriptionUpdate.UpdateNeeded(sub))
|
if (!subscriptionUpdate.UpdateNeeded(sub))
|
||||||
@ -792,34 +791,26 @@ public class StripePaymentService : IPaymentService
|
|||||||
string paymentIntentClientSecret = null;
|
string paymentIntentClientSecret = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);
|
var subItemOptions = updatedItemOptions.Select(itemOption =>
|
||||||
|
new Stripe.InvoiceSubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = itemOption.Id,
|
||||||
|
Plan = itemOption.Plan,
|
||||||
|
Quantity = itemOption.Quantity,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
|
var reviewInvoiceResponse = await PreviewUpcomingInvoiceAndPayAsync(storableSubscriber, subItemOptions);
|
||||||
|
paymentIntentClientSecret = reviewInvoiceResponse.PaymentIntentClientSecret;
|
||||||
|
|
||||||
|
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);
|
||||||
|
var invoice =
|
||||||
|
await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
|
||||||
if (invoice == null)
|
if (invoice == null)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Unable to locate draft invoice for subscription update.");
|
throw new BadRequestException("Unable to locate draft invoice for subscription update.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (chargeNow)
|
|
||||||
{
|
|
||||||
paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync(
|
|
||||||
storableSubscriber, invoice);
|
|
||||||
}
|
}
|
||||||
else
|
catch (Exception e)
|
||||||
{
|
|
||||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions
|
|
||||||
{
|
|
||||||
AutoAdvance = false,
|
|
||||||
});
|
|
||||||
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions());
|
|
||||||
paymentIntentClientSecret = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
{
|
||||||
// Need to revert the subscription
|
// Need to revert the subscription
|
||||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
||||||
@ -833,21 +824,13 @@ public class StripePaymentService : IPaymentService
|
|||||||
});
|
});
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (!invoice.Paid)
|
|
||||||
{
|
|
||||||
// Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h
|
|
||||||
invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId);
|
|
||||||
paymentIntentClientSecret = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// Change back the subscription collection method and/or days until due
|
// Change back the subscription collection method and/or days until due
|
||||||
if (collectionMethod != "send_invoice" || daysUntilDue == null)
|
if (collectionMethod != "send_invoice" || daysUntilDue == null)
|
||||||
{
|
{
|
||||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
|
||||||
|
new Stripe.SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
CollectionMethod = collectionMethod,
|
CollectionMethod = collectionMethod,
|
||||||
DaysUntilDue = daysUntilDue,
|
DaysUntilDue = daysUntilDue,
|
||||||
@ -934,6 +917,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId);
|
await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//This method is no-longer is use because we return the dollar threshold feature on invoice will be generated. but we dont want to lose this implementation.
|
||||||
public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Stripe.Invoice invoice)
|
public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Stripe.Invoice invoice)
|
||||||
{
|
{
|
||||||
var customerOptions = new Stripe.CustomerGetOptions();
|
var customerOptions = new Stripe.CustomerGetOptions();
|
||||||
@ -1103,6 +1087,310 @@ public class StripePaymentService : IPaymentService
|
|||||||
return paymentIntentClientSecret;
|
return paymentIntentClientSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal async Task<InvoicePreviewResult> PreviewUpcomingInvoiceAndPayAsync(ISubscriber subscriber,
|
||||||
|
List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions, int prorateThreshold = 50000)
|
||||||
|
{
|
||||||
|
var customer = await CheckInAppPurchaseMethod(subscriber);
|
||||||
|
|
||||||
|
string paymentIntentClientSecret = null;
|
||||||
|
|
||||||
|
var pendingInvoiceItems = GetPendingInvoiceItems(subscriber);
|
||||||
|
|
||||||
|
var upcomingPreview = await GetUpcomingInvoiceAsync(subscriber, subItemOptions);
|
||||||
|
|
||||||
|
var itemsForInvoice = GetItemsForInvoice(subItemOptions, upcomingPreview, pendingInvoiceItems);
|
||||||
|
var invoiceAmount = itemsForInvoice?.Sum(i => i.Amount) ?? 0;
|
||||||
|
var invoiceNow = invoiceAmount >= prorateThreshold;
|
||||||
|
if (invoiceNow)
|
||||||
|
{
|
||||||
|
await ProcessImmediateInvoiceAsync(subscriber, upcomingPreview, invoiceAmount, customer, itemsForInvoice, pendingInvoiceItems, paymentIntentClientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InvoicePreviewResult { IsInvoicedNow = invoiceNow, PaymentIntentClientSecret = paymentIntentClientSecret };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<InvoicePreviewResult> ProcessImmediateInvoiceAsync(ISubscriber subscriber, Invoice upcomingPreview, long invoiceAmount,
|
||||||
|
Customer customer, IEnumerable<InvoiceLineItem> itemsForInvoice, PendingInoviceItems pendingInvoiceItems,
|
||||||
|
string paymentIntentClientSecret)
|
||||||
|
{
|
||||||
|
// Owes more than prorateThreshold on the next invoice.
|
||||||
|
// Invoice them and pay now instead of waiting until the next billing cycle.
|
||||||
|
|
||||||
|
string cardPaymentMethodId = null;
|
||||||
|
var invoiceAmountDue = upcomingPreview.StartingBalance + invoiceAmount;
|
||||||
|
cardPaymentMethodId = GetCardPaymentMethodId(invoiceAmountDue, customer, cardPaymentMethodId);
|
||||||
|
|
||||||
|
Stripe.Invoice invoice = null;
|
||||||
|
var createdInvoiceItems = new List<Stripe.InvoiceItem>();
|
||||||
|
Braintree.Transaction braintreeTransaction = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await CreateInvoiceItemsAsync(subscriber, itemsForInvoice, pendingInvoiceItems, createdInvoiceItems);
|
||||||
|
|
||||||
|
invoice = await CreateInvoiceAsync(subscriber, cardPaymentMethodId);
|
||||||
|
|
||||||
|
var invoicePayOptions = new Stripe.InvoicePayOptions();
|
||||||
|
await CreateBrainTreeTransactionRequestAsync(subscriber, invoice, customer, invoicePayOptions,
|
||||||
|
cardPaymentMethodId, braintreeTransaction);
|
||||||
|
|
||||||
|
await InvoicePayAsync(invoicePayOptions, invoice, paymentIntentClientSecret);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (braintreeTransaction != null)
|
||||||
|
{
|
||||||
|
await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invoice != null)
|
||||||
|
{
|
||||||
|
if (invoice.Status == "paid")
|
||||||
|
{
|
||||||
|
// It's apparently paid, so we return without throwing an exception
|
||||||
|
return new InvoicePreviewResult
|
||||||
|
{
|
||||||
|
IsInvoicedNow = false,
|
||||||
|
PaymentIntentClientSecret = paymentIntentClientSecret
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await RestoreInvoiceItemsAsync(invoice, customer, pendingInvoiceItems.PendingInvoiceItems);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var ii in createdInvoiceItems)
|
||||||
|
{
|
||||||
|
await _stripeAdapter.InvoiceDeleteAsync(ii.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e is Stripe.StripeException strEx &&
|
||||||
|
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
||||||
|
{
|
||||||
|
throw new GatewayException("Bank account is not yet verified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InvoicePreviewResult
|
||||||
|
{
|
||||||
|
IsInvoicedNow = false,
|
||||||
|
PaymentIntentClientSecret = paymentIntentClientSecret
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<InvoiceLineItem> GetItemsForInvoice(List<InvoiceSubscriptionItemOptions> subItemOptions, Invoice upcomingPreview,
|
||||||
|
PendingInoviceItems pendingInvoiceItems)
|
||||||
|
{
|
||||||
|
var itemsForInvoice = upcomingPreview.Lines?.Data?
|
||||||
|
.Where(i => pendingInvoiceItems.PendingInvoiceItemsDict.ContainsKey(i.Id) ||
|
||||||
|
(i.Plan.Id == subItemOptions[0]?.Plan && i.Proration));
|
||||||
|
return itemsForInvoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PendingInoviceItems GetPendingInvoiceItems(ISubscriber subscriber)
|
||||||
|
{
|
||||||
|
var pendingInvoiceItems = new PendingInoviceItems();
|
||||||
|
var invoiceItems = _stripeAdapter.InvoiceItemListAsync(new Stripe.InvoiceItemListOptions
|
||||||
|
{
|
||||||
|
Customer = subscriber.GatewayCustomerId
|
||||||
|
}).ToList().Where(i => i.InvoiceId == null);
|
||||||
|
pendingInvoiceItems.PendingInvoiceItemsDict = invoiceItems.ToDictionary(pii => pii.Id);
|
||||||
|
return pendingInvoiceItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Customer> CheckInAppPurchaseMethod(ISubscriber subscriber)
|
||||||
|
{
|
||||||
|
var customerOptions = GetCustomerPaymentOptions();
|
||||||
|
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
|
||||||
|
var usingInAppPaymentMethod = customer.Metadata.ContainsKey("appleReceipt");
|
||||||
|
if (usingInAppPaymentMethod)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot perform this action with in-app purchase payment method. " +
|
||||||
|
"Contact support.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCardPaymentMethodId(long invoiceAmountDue, Customer customer, string cardPaymentMethodId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (invoiceAmountDue <= 0 || customer.Metadata.ContainsKey("btCustomerId")) return cardPaymentMethodId;
|
||||||
|
var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card";
|
||||||
|
var hasDefaultValidSource = customer.DefaultSource != null &&
|
||||||
|
(customer.DefaultSource is Stripe.Card ||
|
||||||
|
customer.DefaultSource is Stripe.BankAccount);
|
||||||
|
if (hasDefaultCardPaymentMethod || hasDefaultValidSource) return cardPaymentMethodId;
|
||||||
|
cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id;
|
||||||
|
if (cardPaymentMethodId == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("No payment method is available.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("No payment method is available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return cardPaymentMethodId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Invoice> GetUpcomingInvoiceAsync(ISubscriber subscriber, List<InvoiceSubscriptionItemOptions> subItemOptions)
|
||||||
|
{
|
||||||
|
var upcomingPreview = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
||||||
|
{
|
||||||
|
Customer = subscriber.GatewayCustomerId,
|
||||||
|
Subscription = subscriber.GatewaySubscriptionId,
|
||||||
|
SubscriptionItems = subItemOptions
|
||||||
|
});
|
||||||
|
return upcomingPreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RestoreInvoiceItemsAsync(Invoice invoice, Customer customer, IEnumerable<InvoiceItem> pendingInvoiceItems)
|
||||||
|
{
|
||||||
|
invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new Stripe.InvoiceVoidOptions());
|
||||||
|
if (invoice.StartingBalance != 0)
|
||||||
|
{
|
||||||
|
await _stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||||
|
new Stripe.CustomerUpdateOptions { Balance = customer.Balance });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore invoice items that were brought in
|
||||||
|
foreach (var item in pendingInvoiceItems)
|
||||||
|
{
|
||||||
|
var i = new Stripe.InvoiceItemCreateOptions
|
||||||
|
{
|
||||||
|
Currency = item.Currency,
|
||||||
|
Description = item.Description,
|
||||||
|
Customer = item.CustomerId,
|
||||||
|
Subscription = item.SubscriptionId,
|
||||||
|
Discountable = item.Discountable,
|
||||||
|
Metadata = item.Metadata,
|
||||||
|
Quantity = item.Proration ? 1 : item.Quantity,
|
||||||
|
UnitAmount = item.UnitAmount
|
||||||
|
};
|
||||||
|
await _stripeAdapter.InvoiceItemCreateAsync(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InvoicePayAsync(InvoicePayOptions invoicePayOptions, Invoice invoice, string paymentIntentClientSecret)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions);
|
||||||
|
}
|
||||||
|
catch (Stripe.StripeException e)
|
||||||
|
{
|
||||||
|
if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&
|
||||||
|
e.StripeError?.Code == "invoice_payment_intent_requires_action")
|
||||||
|
{
|
||||||
|
// SCA required, get intent client secret
|
||||||
|
var invoiceGetOptions = new Stripe.InvoiceGetOptions();
|
||||||
|
invoiceGetOptions.AddExpand("payment_intent");
|
||||||
|
invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions);
|
||||||
|
paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new GatewayException("Unable to pay invoice.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateBrainTreeTransactionRequestAsync(ISubscriber subscriber, Invoice invoice, Customer customer,
|
||||||
|
InvoicePayOptions invoicePayOptions, string cardPaymentMethodId, Braintree.Transaction braintreeTransaction)
|
||||||
|
{
|
||||||
|
if (invoice.AmountDue > 0)
|
||||||
|
{
|
||||||
|
if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
|
||||||
|
{
|
||||||
|
invoicePayOptions.PaidOutOfBand = true;
|
||||||
|
var btInvoiceAmount = (invoice.AmountDue / 100M);
|
||||||
|
var transactionResult = await _btGateway.Transaction.SaleAsync(
|
||||||
|
new Braintree.TransactionRequest
|
||||||
|
{
|
||||||
|
Amount = btInvoiceAmount,
|
||||||
|
CustomerId = customer.Metadata["btCustomerId"],
|
||||||
|
Options = new Braintree.TransactionOptionsRequest
|
||||||
|
{
|
||||||
|
SubmitForSettlement = true,
|
||||||
|
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
||||||
|
{
|
||||||
|
CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CustomFields = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
[subscriber.BraintreeIdField()] = subscriber.Id.ToString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!transactionResult.IsSuccess())
|
||||||
|
{
|
||||||
|
throw new GatewayException("Failed to charge PayPal customer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
braintreeTransaction = transactionResult.Target;
|
||||||
|
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions
|
||||||
|
{
|
||||||
|
Metadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["btTransactionId"] = braintreeTransaction.Id,
|
||||||
|
["btPayPalTransactionId"] =
|
||||||
|
braintreeTransaction.PayPalDetails.AuthorizationId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
invoicePayOptions.OffSession = true;
|
||||||
|
invoicePayOptions.PaymentMethod = cardPaymentMethodId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Invoice> CreateInvoiceAsync(ISubscriber subscriber, string cardPaymentMethodId)
|
||||||
|
{
|
||||||
|
Invoice invoice;
|
||||||
|
invoice = await _stripeAdapter.InvoiceCreateAsync(new Stripe.InvoiceCreateOptions
|
||||||
|
{
|
||||||
|
CollectionMethod = "send_invoice",
|
||||||
|
DaysUntilDue = 1,
|
||||||
|
Customer = subscriber.GatewayCustomerId,
|
||||||
|
Subscription = subscriber.GatewaySubscriptionId,
|
||||||
|
DefaultPaymentMethod = cardPaymentMethodId
|
||||||
|
});
|
||||||
|
return invoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateInvoiceItemsAsync(ISubscriber subscriber, IEnumerable<InvoiceLineItem> itemsForInvoice,
|
||||||
|
PendingInoviceItems pendingInvoiceItems, List<InvoiceItem> createdInvoiceItems)
|
||||||
|
{
|
||||||
|
foreach (var invoiceLineItem in itemsForInvoice)
|
||||||
|
{
|
||||||
|
if (pendingInvoiceItems.PendingInvoiceItemsDict.ContainsKey(invoiceLineItem.Id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invoiceItem = await _stripeAdapter.InvoiceItemCreateAsync(new Stripe.InvoiceItemCreateOptions
|
||||||
|
{
|
||||||
|
Currency = invoiceLineItem.Currency,
|
||||||
|
Description = invoiceLineItem.Description,
|
||||||
|
Customer = subscriber.GatewayCustomerId,
|
||||||
|
Subscription = invoiceLineItem.Subscription,
|
||||||
|
Discountable = invoiceLineItem.Discountable,
|
||||||
|
Amount = invoiceLineItem.Amount
|
||||||
|
});
|
||||||
|
createdInvoiceItems.Add(invoiceItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
|
public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
|
||||||
bool skipInAppPurchaseCheck = false)
|
bool skipInAppPurchaseCheck = false)
|
||||||
{
|
{
|
||||||
|
@ -6,7 +6,7 @@ using Bit.Core.Models.StaticStore.Plans;
|
|||||||
|
|
||||||
namespace Bit.Core.Utilities;
|
namespace Bit.Core.Utilities;
|
||||||
|
|
||||||
public class StaticStore
|
public static class StaticStore
|
||||||
{
|
{
|
||||||
static StaticStore()
|
static StaticStore()
|
||||||
{
|
{
|
||||||
@ -112,6 +112,11 @@ public class StaticStore
|
|||||||
new EnterprisePlan(false),
|
new EnterprisePlan(false),
|
||||||
new TeamsPlan(true),
|
new TeamsPlan(true),
|
||||||
new TeamsPlan(false),
|
new TeamsPlan(false),
|
||||||
|
|
||||||
|
new Enterprise2020Plan(true),
|
||||||
|
new Enterprise2020Plan(false),
|
||||||
|
new Teams2020Plan(true),
|
||||||
|
new Teams2020Plan(false),
|
||||||
new FamiliesPlan(),
|
new FamiliesPlan(),
|
||||||
new FreePlan(),
|
new FreePlan(),
|
||||||
new CustomPlan(),
|
new CustomPlan(),
|
||||||
@ -139,8 +144,7 @@ public class StaticStore
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Models.StaticStore.Plan GetPlan(PlanType planType) =>
|
public static Plan GetPlan(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType);
|
||||||
Plans.SingleOrDefault(p => p.Type == planType);
|
|
||||||
|
|
||||||
|
|
||||||
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>
|
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>
|
||||||
|
@ -95,17 +95,22 @@ public class OrganizationRepository : Repository<Core.Entities.Organization, Org
|
|||||||
|
|
||||||
public async Task<ICollection<Core.Entities.Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take)
|
public async Task<ICollection<Core.Entities.Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
{
|
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var query = from o in dbContext.Organizations
|
var query = from o in dbContext.Organizations
|
||||||
where o.PlanType >= PlanType.TeamsMonthly && o.PlanType <= PlanType.EnterpriseAnnually &&
|
where o.PlanType >= PlanType.TeamsMonthly2020 && o.PlanType <= PlanType.EnterpriseAnnually &&
|
||||||
!dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) &&
|
!dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) &&
|
||||||
(string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%"))
|
(string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%"))
|
||||||
select o;
|
select o;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(ownerEmail))
|
if (string.IsNullOrWhiteSpace(ownerEmail))
|
||||||
{
|
{
|
||||||
|
return await query.OrderByDescending(o => o.CreationDate)
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(take)
|
||||||
|
.ToArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
if (dbContext.Database.IsNpgsql())
|
if (dbContext.Database.IsNpgsql())
|
||||||
{
|
{
|
||||||
query = from o in query
|
query = from o in query
|
||||||
@ -126,11 +131,9 @@ public class OrganizationRepository : Repository<Core.Entities.Organization, Org
|
|||||||
where ou.Type == OrganizationUserType.Owner && EF.Functions.Like(u.Email, $"{ownerEmail}%")
|
where ou.Type == OrganizationUserType.Owner && EF.Functions.Like(u.Email, $"{ownerEmail}%")
|
||||||
select o;
|
select o;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return await query.OrderByDescending(o => o.CreationDate).Skip(skip).Take(take).ToArrayAsync();
|
return await query.OrderByDescending(o => o.CreationDate).Skip(skip).Take(take).ToArrayAsync();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateStorageAsync(Guid id)
|
public async Task UpdateStorageAsync(Guid id)
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using AutoFixture.Xunit2;
|
using AutoFixture.Xunit2;
|
||||||
using Bit.Api.AdminConsole.Controllers;
|
using Bit.Api.AdminConsole.Controllers;
|
||||||
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
@ -10,13 +12,17 @@ using Bit.Core.Auth.Repositories;
|
|||||||
using Bit.Core.Auth.Services;
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||||
@ -145,4 +151,194 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
await _organizationService.DeleteUserAsync(orgId, user.Id);
|
await _organizationService.DeleteUserAsync(orgId, user.Id);
|
||||||
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
|
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
OrganizationUpgradeRequestModel model)
|
||||||
|
{
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(false);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostUpgrade(organizationId.ToString(), model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_NonSMUpgrade_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
OrganizationUpgradeRequestModel model,
|
||||||
|
bool success,
|
||||||
|
string paymentIntentClientSecret)
|
||||||
|
{
|
||||||
|
model.UseSecretsManager = false;
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||||
|
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||||
|
|
||||||
|
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
||||||
|
|
||||||
|
Assert.Equal(success, response.Success);
|
||||||
|
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_SMUpgrade_ProvidesAccess_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUpgradeRequestModel model,
|
||||||
|
bool success,
|
||||||
|
string paymentIntentClientSecret,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
model.UseSecretsManager = true;
|
||||||
|
organizationUser.AccessSecretsManager = false;
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||||
|
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).Returns(organizationUser);
|
||||||
|
|
||||||
|
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
||||||
|
|
||||||
|
Assert.Equal(success, response.Success);
|
||||||
|
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||||
|
|
||||||
|
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
||||||
|
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostUpgrade_SMUpgrade_NullOrgUser_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUpgradeRequestModel model,
|
||||||
|
bool success,
|
||||||
|
string paymentIntentClientSecret)
|
||||||
|
{
|
||||||
|
model.UseSecretsManager = true;
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>())
|
||||||
|
.Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organizationId, userId).ReturnsNull();
|
||||||
|
|
||||||
|
var response = await _sut.PostUpgrade(organizationId.ToString(), model);
|
||||||
|
|
||||||
|
Assert.Equal(success, response.Success);
|
||||||
|
Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);
|
||||||
|
|
||||||
|
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrg_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model)
|
||||||
|
{
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_UserCannotEditSubscription_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model,
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(false);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_ProvidesAccess_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model,
|
||||||
|
Organization organization,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUser organizationUser,
|
||||||
|
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
||||||
|
{
|
||||||
|
organizationUser.AccessSecretsManager = false;
|
||||||
|
|
||||||
|
var ssoConfigurationData = new SsoConfigurationData
|
||||||
|
{
|
||||||
|
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
||||||
|
KeyConnectorUrl = "https://example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
organizationUserOrganizationDetails.Permissions = string.Empty;
|
||||||
|
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).Returns(organizationUser);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(organizationUserOrganizationDetails);
|
||||||
|
|
||||||
|
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
||||||
|
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
||||||
|
|
||||||
|
await _addSecretsManagerSubscriptionCommand.Received(1)
|
||||||
|
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
||||||
|
await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>
|
||||||
|
orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrgUser_ReturnsCorrectResponse(
|
||||||
|
Guid organizationId,
|
||||||
|
SecretsManagerSubscribeRequestModel model,
|
||||||
|
Organization organization,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUserOrganizationDetails organizationUserOrganizationDetails)
|
||||||
|
{
|
||||||
|
var ssoConfigurationData = new SsoConfigurationData
|
||||||
|
{
|
||||||
|
MemberDecryptionType = MemberDecryptionType.KeyConnector,
|
||||||
|
KeyConnectorUrl = "https://example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
organizationUserOrganizationDetails.Permissions = string.Empty;
|
||||||
|
organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId).Returns(organization);
|
||||||
|
|
||||||
|
_currentContext.EditSubscription(organizationId).Returns(true);
|
||||||
|
|
||||||
|
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
|
||||||
|
_organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).ReturnsNull();
|
||||||
|
|
||||||
|
_organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(organizationUserOrganizationDetails);
|
||||||
|
|
||||||
|
var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);
|
||||||
|
|
||||||
|
Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);
|
||||||
|
Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);
|
||||||
|
|
||||||
|
await _addSecretsManagerSubscriptionCommand.Received(1)
|
||||||
|
.SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);
|
||||||
|
await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Api.Auth.Models.Request.Accounts;
|
|||||||
using Bit.Api.Auth.Models.Request.Webauthn;
|
using Bit.Api.Auth.Models.Request.Webauthn;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
@ -59,6 +60,21 @@ public class WebAuthnControllerTests
|
|||||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task PostOptions_RequireSsoPolicyApplicable_ThrowsBadRequestException(
|
||||||
|
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||||
|
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);
|
||||||
|
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(true);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.PostOptions(requestModel));
|
||||||
|
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnCredentialRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnCredentialRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
@ -113,6 +129,32 @@ public class WebAuthnControllerTests
|
|||||||
// Nothing to assert since return is void
|
// Nothing to assert since return is void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Post_RequireSsoPolicyApplicable_ThrowsBadRequestException(
|
||||||
|
WebAuthnCredentialRequestModel requestModel,
|
||||||
|
CredentialCreateOptions createOptions,
|
||||||
|
User user,
|
||||||
|
SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetUserByPrincipalAsync(default)
|
||||||
|
.ReturnsForAnyArgs(user);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>())
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||||
|
.Unprotect(requestModel.Token)
|
||||||
|
.Returns(token);
|
||||||
|
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(true);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.Post(requestModel));
|
||||||
|
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
|
@ -133,8 +133,8 @@ public class SecretsManagerOrganizationCustomization : ICustomization
|
|||||||
{
|
{
|
||||||
public void Customize(IFixture fixture)
|
public void Customize(IFixture fixture)
|
||||||
{
|
{
|
||||||
|
const PlanType planType = PlanType.EnterpriseAnnually;
|
||||||
var organizationId = Guid.NewGuid();
|
var organizationId = Guid.NewGuid();
|
||||||
var planType = PlanType.EnterpriseAnnually;
|
|
||||||
|
|
||||||
fixture.Customize<Organization>(composer => composer
|
fixture.Customize<Organization>(composer => composer
|
||||||
.With(o => o.Id, organizationId)
|
.With(o => o.Id, organizationId)
|
||||||
@ -143,8 +143,7 @@ public class SecretsManagerOrganizationCustomization : ICustomization
|
|||||||
.With(o => o.PlanType, planType)
|
.With(o => o.PlanType, planType)
|
||||||
.With(o => o.Plan, StaticStore.GetPlan(planType).Name)
|
.With(o => o.Plan, StaticStore.GetPlan(planType).Name)
|
||||||
.With(o => o.MaxAutoscaleSmSeats, (int?)null)
|
.With(o => o.MaxAutoscaleSmSeats, (int?)null)
|
||||||
.With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null)
|
.With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,17 +15,45 @@ public class SecretsManagerSubscriptionUpdateTests
|
|||||||
[BitAutoData(PlanType.Custom)]
|
[BitAutoData(PlanType.Custom)]
|
||||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||||
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
public Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
|
||||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
|
||||||
[BitAutoData(PlanType.TeamsAnnually2019)]
|
|
||||||
public async Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
|
|
||||||
PlanType planType,
|
PlanType planType,
|
||||||
Organization organization)
|
Organization organization)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
organization.PlanType = planType;
|
organization.PlanType = planType;
|
||||||
|
|
||||||
|
// Act
|
||||||
var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, false));
|
var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, false));
|
||||||
|
|
||||||
|
// Assert
|
||||||
Assert.Contains("Invalid Secrets Manager plan", exception.Message, StringComparison.InvariantCultureIgnoreCase);
|
Assert.Contains("Invalid Secrets Manager plan", exception.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
public void UpdateSubscription_WithNonSecretsManagerPlanType_DoesNotThrowException(
|
||||||
|
PlanType planType,
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organization.PlanType = planType;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var ex = Record.Exception(() => new SecretsManagerSubscriptionUpdate(organization, false));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -739,4 +739,300 @@ public class StripePaymentServiceTests
|
|||||||
|
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task PreviewUpcomingInvoiceAndPayAsync_WithInAppPaymentMethod_ThrowsBadRequestException(SutProvider<StripePaymentService> sutProvider,
|
||||||
|
Organization subscriber, List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions)
|
||||||
|
{
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.CustomerGetAsync(Arg.Any<string>(), Arg.Any<Stripe.CustomerGetOptions>())
|
||||||
|
.Returns(new Stripe.Customer { Metadata = new Dictionary<string, string> { { "appleReceipt", "dummyData" } } });
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, subItemOptions));
|
||||||
|
Assert.Equal("Cannot perform this action with in-app purchase payment method. Contact support.", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void PreviewUpcomingInvoiceAndPayAsync_UpcomingInvoiceBelowThreshold_DoesNotInvoiceNow(SutProvider<StripePaymentService> sutProvider,
|
||||||
|
Organization subscriber, List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions)
|
||||||
|
{
|
||||||
|
var prorateThreshold = 50000;
|
||||||
|
var invoiceAmountBelowThreshold = prorateThreshold - 100;
|
||||||
|
var customer = MockStripeCustomer(subscriber);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(default, default).ReturnsForAnyArgs(customer);
|
||||||
|
var invoiceItem = MockInoviceItemList(subscriber, "planId", invoiceAmountBelowThreshold, customer);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().InvoiceItemListAsync(new Stripe.InvoiceItemListOptions
|
||||||
|
{
|
||||||
|
Customer = subscriber.GatewayCustomerId
|
||||||
|
}).ReturnsForAnyArgs(invoiceItem);
|
||||||
|
|
||||||
|
var invoiceLineItem = CreateInvoiceLineTime(subscriber, "planId", invoiceAmountBelowThreshold);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
||||||
|
{
|
||||||
|
Customer = subscriber.GatewayCustomerId,
|
||||||
|
Subscription = subscriber.GatewaySubscriptionId,
|
||||||
|
SubscriptionItems = subItemOptions
|
||||||
|
}).ReturnsForAnyArgs(invoiceLineItem);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().InvoiceCreateAsync(Arg.Is<Stripe.InvoiceCreateOptions>(options =>
|
||||||
|
options.CollectionMethod == "send_invoice" &&
|
||||||
|
options.DaysUntilDue == 1 &&
|
||||||
|
options.Customer == subscriber.GatewayCustomerId &&
|
||||||
|
options.Subscription == subscriber.GatewaySubscriptionId &&
|
||||||
|
options.DefaultPaymentMethod == customer.InvoiceSettings.DefaultPaymentMethod.Id
|
||||||
|
)).ReturnsForAnyArgs(new Stripe.Invoice
|
||||||
|
{
|
||||||
|
Id = "mockInvoiceId",
|
||||||
|
CollectionMethod = "send_invoice",
|
||||||
|
DueDate = DateTime.Now.AddDays(1),
|
||||||
|
Customer = customer,
|
||||||
|
Subscription = new Stripe.Subscription
|
||||||
|
{
|
||||||
|
Id = "mockSubscriptionId",
|
||||||
|
Customer = customer,
|
||||||
|
Status = "active",
|
||||||
|
CurrentPeriodStart = DateTime.UtcNow,
|
||||||
|
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1),
|
||||||
|
CollectionMethod = "charge_automatically",
|
||||||
|
},
|
||||||
|
DefaultPaymentMethod = customer.InvoiceSettings.DefaultPaymentMethod,
|
||||||
|
AmountDue = invoiceAmountBelowThreshold,
|
||||||
|
Currency = "usd",
|
||||||
|
Status = "draft",
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, new List<Stripe.InvoiceSubscriptionItemOptions>(), prorateThreshold);
|
||||||
|
|
||||||
|
Assert.False(result.IsInvoicedNow);
|
||||||
|
Assert.Null(result.PaymentIntentClientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void PreviewUpcomingInvoiceAndPayAsync_NoPaymentMethod_ThrowsBadRequestException(SutProvider<StripePaymentService> sutProvider,
|
||||||
|
Organization subscriber, List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions, string planId)
|
||||||
|
{
|
||||||
|
var prorateThreshold = 120000;
|
||||||
|
var invoiceAmountBelowThreshold = prorateThreshold;
|
||||||
|
var customer = new Stripe.Customer
|
||||||
|
{
|
||||||
|
Metadata = new Dictionary<string, string>(),
|
||||||
|
Id = subscriber.GatewayCustomerId,
|
||||||
|
DefaultSource = null,
|
||||||
|
InvoiceSettings = new Stripe.CustomerInvoiceSettings
|
||||||
|
{
|
||||||
|
DefaultPaymentMethod = null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(default, default).ReturnsForAnyArgs(customer);
|
||||||
|
var invoiceItem = MockInoviceItemList(subscriber, planId, invoiceAmountBelowThreshold, customer);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().InvoiceItemListAsync(new Stripe.InvoiceItemListOptions
|
||||||
|
{
|
||||||
|
Customer = subscriber.GatewayCustomerId
|
||||||
|
}).ReturnsForAnyArgs(invoiceItem);
|
||||||
|
|
||||||
|
var invoiceLineItem = CreateInvoiceLineTime(subscriber, planId, invoiceAmountBelowThreshold);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
||||||
|
{
|
||||||
|
Customer = subscriber.GatewayCustomerId,
|
||||||
|
Subscription = subscriber.GatewaySubscriptionId,
|
||||||
|
SubscriptionItems = subItemOptions
|
||||||
|
}).ReturnsForAnyArgs(invoiceLineItem);
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, subItemOptions));
|
||||||
|
Assert.Equal("No payment method is available.", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void PreviewUpcomingInvoiceAndPayAsync_UpcomingInvoiceAboveThreshold_DoesInvoiceNow(SutProvider<StripePaymentService> sutProvider,
|
||||||
|
Organization subscriber, List<Stripe.InvoiceSubscriptionItemOptions> subItemOptions, string planId)
|
||||||
|
{
|
||||||
|
var prorateThreshold = 50000;
|
||||||
|
var invoiceAmountBelowThreshold = 100000;
|
||||||
|
var customer = MockStripeCustomer(subscriber);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(default, default).ReturnsForAnyArgs(customer);
|
||||||
|
var invoiceItem = MockInoviceItemList(subscriber, planId, invoiceAmountBelowThreshold, customer);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().InvoiceItemListAsync(new Stripe.InvoiceItemListOptions
|
||||||
|
{
|
||||||
|
Customer = subscriber.GatewayCustomerId
|
||||||
|
}).ReturnsForAnyArgs(invoiceItem);
|
||||||
|
|
||||||
|
var invoiceLineItem = CreateInvoiceLineTime(subscriber, planId, invoiceAmountBelowThreshold);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
||||||
|
{
|
||||||
|
Customer = subscriber.GatewayCustomerId,
|
||||||
|
Subscription = subscriber.GatewaySubscriptionId,
|
||||||
|
SubscriptionItems = subItemOptions
|
||||||
|
}).ReturnsForAnyArgs(invoiceLineItem);
|
||||||
|
|
||||||
|
var invoice = MockInVoice(customer, invoiceAmountBelowThreshold);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().InvoiceCreateAsync(Arg.Is<Stripe.InvoiceCreateOptions>(options =>
|
||||||
|
options.CollectionMethod == "send_invoice" &&
|
||||||
|
options.DaysUntilDue == 1 &&
|
||||||
|
options.Customer == subscriber.GatewayCustomerId &&
|
||||||
|
options.Subscription == subscriber.GatewaySubscriptionId &&
|
||||||
|
options.DefaultPaymentMethod == customer.InvoiceSettings.DefaultPaymentMethod.Id
|
||||||
|
)).ReturnsForAnyArgs(invoice);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, new List<Stripe.InvoiceSubscriptionItemOptions>(), prorateThreshold);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IStripeAdapter>().Received(1).InvoicePayAsync(invoice.Id,
|
||||||
|
Arg.Is<Stripe.InvoicePayOptions>((options =>
|
||||||
|
options.OffSession == true
|
||||||
|
)));
|
||||||
|
|
||||||
|
|
||||||
|
Assert.True(result.IsInvoicedNow);
|
||||||
|
Assert.Null(result.PaymentIntentClientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stripe.Invoice MockInVoice(Stripe.Customer customer, int invoiceAmountBelowThreshold) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "mockInvoiceId",
|
||||||
|
CollectionMethod = "send_invoice",
|
||||||
|
DueDate = DateTime.Now.AddDays(1),
|
||||||
|
Customer = customer,
|
||||||
|
Subscription = new Stripe.Subscription
|
||||||
|
{
|
||||||
|
Id = "mockSubscriptionId",
|
||||||
|
Customer = customer,
|
||||||
|
Status = "active",
|
||||||
|
CurrentPeriodStart = DateTime.UtcNow,
|
||||||
|
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1),
|
||||||
|
CollectionMethod = "charge_automatically",
|
||||||
|
},
|
||||||
|
DefaultPaymentMethod = customer.InvoiceSettings.DefaultPaymentMethod,
|
||||||
|
AmountDue = invoiceAmountBelowThreshold,
|
||||||
|
Currency = "usd",
|
||||||
|
Status = "draft",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static List<Stripe.InvoiceItem> MockInoviceItemList(Organization subscriber, string planId, int invoiceAmountBelowThreshold, Stripe.Customer customer) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
new Stripe.InvoiceItem
|
||||||
|
{
|
||||||
|
Id = "ii_1234567890",
|
||||||
|
Amount = invoiceAmountBelowThreshold,
|
||||||
|
Currency = "usd",
|
||||||
|
CustomerId = subscriber.GatewayCustomerId,
|
||||||
|
Description = "Sample invoice item 1",
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
Discountable = true,
|
||||||
|
InvoiceId = "548458365"
|
||||||
|
},
|
||||||
|
new Stripe.InvoiceItem
|
||||||
|
{
|
||||||
|
Id = "ii_0987654321",
|
||||||
|
Amount = invoiceAmountBelowThreshold,
|
||||||
|
Currency = "usd",
|
||||||
|
CustomerId = customer.Id,
|
||||||
|
Description = "Sample invoice item 2",
|
||||||
|
Date = DateTime.UtcNow.AddDays(-5),
|
||||||
|
Discountable = false,
|
||||||
|
InvoiceId = null,
|
||||||
|
Proration = true,
|
||||||
|
Plan = new Stripe.Plan
|
||||||
|
{
|
||||||
|
Id = planId,
|
||||||
|
Amount = invoiceAmountBelowThreshold,
|
||||||
|
Currency = "usd",
|
||||||
|
Interval = "month",
|
||||||
|
IntervalCount = 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Stripe.Customer MockStripeCustomer(Organization subscriber)
|
||||||
|
{
|
||||||
|
var customer = new Stripe.Customer
|
||||||
|
{
|
||||||
|
Metadata = new Dictionary<string, string>(),
|
||||||
|
Id = subscriber.GatewayCustomerId,
|
||||||
|
DefaultSource = new Stripe.Card
|
||||||
|
{
|
||||||
|
Id = "card_12345",
|
||||||
|
Last4 = "1234",
|
||||||
|
Brand = "Visa",
|
||||||
|
ExpYear = 2025,
|
||||||
|
ExpMonth = 12
|
||||||
|
},
|
||||||
|
InvoiceSettings = new Stripe.CustomerInvoiceSettings
|
||||||
|
{
|
||||||
|
DefaultPaymentMethod = new Stripe.PaymentMethod
|
||||||
|
{
|
||||||
|
Id = "pm_12345",
|
||||||
|
Type = "card",
|
||||||
|
Card = new Stripe.PaymentMethodCard
|
||||||
|
{
|
||||||
|
Last4 = "1234",
|
||||||
|
Brand = "Visa",
|
||||||
|
ExpYear = 2025,
|
||||||
|
ExpMonth = 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stripe.Invoice CreateInvoiceLineTime(Organization subscriber, string planId, int invoiceAmountBelowThreshold) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
AmountDue = invoiceAmountBelowThreshold,
|
||||||
|
AmountPaid = 0,
|
||||||
|
AmountRemaining = invoiceAmountBelowThreshold,
|
||||||
|
CustomerId = subscriber.GatewayCustomerId,
|
||||||
|
SubscriptionId = subscriber.GatewaySubscriptionId,
|
||||||
|
ApplicationFeeAmount = 0,
|
||||||
|
Currency = "usd",
|
||||||
|
Description = "Upcoming Invoice",
|
||||||
|
Discount = null,
|
||||||
|
DueDate = DateTime.UtcNow.AddDays(1),
|
||||||
|
EndingBalance = 0,
|
||||||
|
Number = "INV12345",
|
||||||
|
Paid = false,
|
||||||
|
PeriodStart = DateTime.UtcNow,
|
||||||
|
PeriodEnd = DateTime.UtcNow.AddMonths(1),
|
||||||
|
ReceiptNumber = null,
|
||||||
|
StartingBalance = 0,
|
||||||
|
Status = "draft",
|
||||||
|
Id = "ii_0987654321",
|
||||||
|
Total = invoiceAmountBelowThreshold,
|
||||||
|
Lines = new Stripe.StripeList<Stripe.InvoiceLineItem>
|
||||||
|
{
|
||||||
|
Data = new List<Stripe.InvoiceLineItem>
|
||||||
|
{
|
||||||
|
new Stripe.InvoiceLineItem
|
||||||
|
{
|
||||||
|
Amount = invoiceAmountBelowThreshold,
|
||||||
|
Currency = "usd",
|
||||||
|
Description = "Sample line item",
|
||||||
|
Id = "ii_0987654321",
|
||||||
|
Livemode = false,
|
||||||
|
Object = "line_item",
|
||||||
|
Discountable = false,
|
||||||
|
Period = new Stripe.InvoiceLineItemPeriod()
|
||||||
|
{
|
||||||
|
Start = DateTime.UtcNow,
|
||||||
|
End = DateTime.UtcNow.AddMonths(1)
|
||||||
|
},
|
||||||
|
Plan = new Stripe.Plan
|
||||||
|
{
|
||||||
|
Id = planId,
|
||||||
|
Amount = invoiceAmountBelowThreshold,
|
||||||
|
Currency = "usd",
|
||||||
|
Interval = "month",
|
||||||
|
IntervalCount = 1,
|
||||||
|
},
|
||||||
|
Proration = true,
|
||||||
|
Quantity = 1,
|
||||||
|
Subscription = subscriber.GatewaySubscriptionId,
|
||||||
|
SubscriptionItem = "si_12345",
|
||||||
|
Type = "subscription",
|
||||||
|
UnitAmountExcludingTax = invoiceAmountBelowThreshold,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,10 @@ public class StaticStoreTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void StaticStore_Initialization_Success()
|
public void StaticStore_Initialization_Success()
|
||||||
{
|
{
|
||||||
var plans = StaticStore.Plans;
|
var plans = StaticStore.Plans.ToList();
|
||||||
Assert.NotNull(plans);
|
Assert.NotNull(plans);
|
||||||
Assert.NotEmpty(plans);
|
Assert.NotEmpty(plans);
|
||||||
Assert.Equal(12, plans.Count());
|
Assert.Equal(16, plans.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
BEGIN TRY
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Organization]
|
||||||
|
SET
|
||||||
|
[Use2fa] = 1,
|
||||||
|
[UseApi] = 1,
|
||||||
|
[UseDirectory] = 1,
|
||||||
|
[UseEvents] = 1,
|
||||||
|
[UseGroups] = 1,
|
||||||
|
[UsersGetPremium] = 1
|
||||||
|
WHERE
|
||||||
|
[PlanType] IN (2, 3); -- Teams 2019
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
END TRY
|
||||||
|
BEGIN CATCH
|
||||||
|
ROLLBACK TRANSACTION;
|
||||||
|
THROW;
|
||||||
|
END CATCH
|
@ -0,0 +1,19 @@
|
|||||||
|
BEGIN TRY
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Organization]
|
||||||
|
SET
|
||||||
|
[UseSso] = 1,
|
||||||
|
[UseKeyConnector] = 1,
|
||||||
|
[UseScim] = 1,
|
||||||
|
[UseResetPassword] = 1
|
||||||
|
WHERE
|
||||||
|
[PlanType] IN (4, 5) -- Enterprise 2019
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
END TRY
|
||||||
|
BEGIN CATCH
|
||||||
|
ROLLBACK TRANSACTION;
|
||||||
|
THROW;
|
||||||
|
END CATCH
|
@ -0,0 +1,16 @@
|
|||||||
|
BEGIN TRY
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Organization]
|
||||||
|
SET
|
||||||
|
[UsersGetPremium] = 1
|
||||||
|
WHERE
|
||||||
|
[PlanType] = 1 -- Families 2019 Annual
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
END TRY
|
||||||
|
BEGIN CATCH
|
||||||
|
ROLLBACK TRANSACTION;
|
||||||
|
THROW;
|
||||||
|
END CATCH
|
Reference in New Issue
Block a user