1
0
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:
Rui Tome
2023-11-02 14:52:14 +00:00
45 changed files with 1481 additions and 178 deletions

View File

@ -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)

View File

@ -215,7 +215,10 @@ public class OrganizationsController : Controller
try try
{ {
await _stripeSyncService.UpdateCustomerEmailAddress(organization.GatewayCustomerId, organization.BillingEmail); if (!string.IsNullOrWhiteSpace(organization.GatewayCustomerId) && !string.IsNullOrWhiteSpace(organization.BillingEmail))
{
await _stripeSyncService.UpdateCustomerEmailAddress(organization.GatewayCustomerId, organization.BillingEmail);
}
} }
catch (StripeException stripeException) catch (StripeException stripeException)
{ {

View File

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

View File

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

View File

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

View File

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

View File

@ -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
{ {

View File

@ -1,4 +1,4 @@
namespace Bit.Api.Auth.Models.Request; namespace Bit.Api.AdminConsole.Models.Request;
public class BulkDenyAdminAuthRequestRequestModel public class BulkDenyAdminAuthRequestRequestModel
{ {

View File

@ -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
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.");
} }

View File

@ -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()
{ {

View File

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

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Business;
public class InvoicePreviewResult
{
public bool IsInvoicedNow { get; set; }
public string PaymentIntentClientSecret { get; set; }
}

View File

@ -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 ||
( (

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

View File

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

View File

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

View File

@ -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)

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

View File

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

View File

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

View File

@ -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)

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,66 +791,50 @@ 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)) catch (Exception e)
{
// Need to revert the subscription
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
{ {
try Items = subscriptionUpdate.RevertItemsOptions(sub),
{ // This proration behavior prevents a false "credit" from
if (chargeNow) // being applied forward to the next month's invoice
{ ProrationBehavior = "none",
paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync( CollectionMethod = collectionMethod,
storableSubscriber, invoice); DaysUntilDue = daysUntilDue,
} });
else throw;
{
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
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
{
Items = subscriptionUpdate.RevertItemsOptions(sub),
// This proration behavior prevents a false "credit" from
// being applied forward to the next month's invoice
ProrationBehavior = "none",
CollectionMethod = collectionMethod,
DaysUntilDue = daysUntilDue,
});
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, {
DaysUntilDue = daysUntilDue, CollectionMethod = collectionMethod,
}); 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)
{ {

View File

@ -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) =>

View File

@ -95,41 +95,44 @@ 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 query = from o in dbContext.Organizations
where o.PlanType >= PlanType.TeamsMonthly2020 && o.PlanType <= PlanType.EnterpriseAnnually &&
!dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) &&
(string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%"))
select o;
if (string.IsNullOrWhiteSpace(ownerEmail))
{ {
var dbContext = GetDatabaseContext(scope); return await query.OrderByDescending(o => o.CreationDate)
var query = from o in dbContext.Organizations .Skip(skip)
where o.PlanType >= PlanType.TeamsMonthly && o.PlanType <= PlanType.EnterpriseAnnually && .Take(take)
!dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) && .ToArrayAsync();
(string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%"))
select o;
if (!string.IsNullOrWhiteSpace(ownerEmail))
{
if (dbContext.Database.IsNpgsql())
{
query = from o in query
join ou in dbContext.OrganizationUsers
on o.Id equals ou.OrganizationId
join u in dbContext.Users
on ou.UserId equals u.Id
where ou.Type == OrganizationUserType.Owner && EF.Functions.ILike(EF.Functions.Collate(u.Email, "default"), $"{ownerEmail}%")
select o;
}
else
{
query = from o in query
join ou in dbContext.OrganizationUsers
on o.Id equals ou.OrganizationId
join u in dbContext.Users
on ou.UserId equals u.Id
where ou.Type == OrganizationUserType.Owner && EF.Functions.Like(u.Email, $"{ownerEmail}%")
select o;
}
}
return await query.OrderByDescending(o => o.CreationDate).Skip(skip).Take(take).ToArrayAsync();
} }
if (dbContext.Database.IsNpgsql())
{
query = from o in query
join ou in dbContext.OrganizationUsers
on o.Id equals ou.OrganizationId
join u in dbContext.Users
on ou.UserId equals u.Id
where ou.Type == OrganizationUserType.Owner && EF.Functions.ILike(EF.Functions.Collate(u.Email, "default"), $"{ownerEmail}%")
select o;
}
else
{
query = from o in query
join ou in dbContext.OrganizationUsers
on o.Id equals ou.OrganizationId
join u in dbContext.Users
on ou.UserId equals u.Id
where ou.Type == OrganizationUserType.Owner && EF.Functions.Like(u.Email, $"{ownerEmail}%")
select o;
}
return await query.OrderByDescending(o => o.CreationDate).Skip(skip).Take(take).ToArrayAsync();
} }
public async Task UpdateStorageAsync(Guid id) public async Task UpdateStorageAsync(Guid id)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]

View File

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

View File

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

View File

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