mirror of
https://github.com/bitwarden/server.git
synced 2025-05-22 20:11:04 -05:00
Merge branch 'main' into add-userid-to-encryption-methods
This commit is contained in:
commit
0f4c04347b
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -636,7 +636,9 @@ jobs:
|
|||||||
|
|
||||||
setup-ephemeral-environment:
|
setup-ephemeral-environment:
|
||||||
name: Setup Ephemeral Environment
|
name: Setup Ephemeral Environment
|
||||||
needs: build-docker
|
needs:
|
||||||
|
- build-artifacts
|
||||||
|
- build-docker
|
||||||
if: |
|
if: |
|
||||||
needs.build-artifacts.outputs.has_secrets == 'true'
|
needs.build-artifacts.outputs.has_secrets == 'true'
|
||||||
&& github.event_name == 'pull_request'
|
&& github.event_name == 'pull_request'
|
||||||
|
@ -3,9 +3,9 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
|||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
@ -7,14 +7,12 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Tax.Services;
|
|
||||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
@ -24,7 +22,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
|
||||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||||
private readonly IStripeAdapter _stripeAdapter;
|
private readonly IStripeAdapter _stripeAdapter;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
@ -32,26 +29,22 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
private readonly ISubscriberService _subscriberService;
|
private readonly ISubscriberService _subscriberService;
|
||||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly IAutomaticTaxStrategy _automaticTaxStrategy;
|
|
||||||
|
|
||||||
public RemoveOrganizationFromProviderCommand(
|
public RemoveOrganizationFromProviderCommand(
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationService organizationService,
|
|
||||||
IProviderOrganizationRepository providerOrganizationRepository,
|
IProviderOrganizationRepository providerOrganizationRepository,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IProviderBillingService providerBillingService,
|
IProviderBillingService providerBillingService,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient)
|
||||||
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
|
|
||||||
{
|
{
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationService = organizationService;
|
|
||||||
_providerOrganizationRepository = providerOrganizationRepository;
|
_providerOrganizationRepository = providerOrganizationRepository;
|
||||||
_stripeAdapter = stripeAdapter;
|
_stripeAdapter = stripeAdapter;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
@ -59,7 +52,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
_subscriberService = subscriberService;
|
_subscriberService = subscriberService;
|
||||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
_automaticTaxStrategy = automaticTaxStrategy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveOrganizationFromProvider(
|
public async Task RemoveOrganizationFromProvider(
|
||||||
@ -77,7 +69,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
|
|
||||||
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
|
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
|
||||||
providerOrganization.OrganizationId,
|
providerOrganization.OrganizationId,
|
||||||
Array.Empty<Guid>(),
|
[],
|
||||||
includeProvider: false))
|
includeProvider: false))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
||||||
@ -102,7 +94,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled
|
/// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled
|
||||||
/// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because
|
/// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because
|
||||||
/// the provider's payment method will be removed from their Stripe customer causing ensuing charges to fail. Lastly,
|
/// the provider's payment method will be removed from their Stripe customer, causing ensuing charges to fail. Lastly,
|
||||||
/// we email the organization owners letting them know they need to add a new payment method.
|
/// we email the organization owners letting them know they need to add a new payment method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ResetOrganizationBillingAsync(
|
private async Task ResetOrganizationBillingAsync(
|
||||||
@ -142,15 +134,18 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
|
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
var setNonUSBusinessUseToReverseCharge = _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge)
|
||||||
{
|
{
|
||||||
_automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
}
|
}
|
||||||
else
|
else if (customer.HasRecognizedTaxLocation())
|
||||||
{
|
{
|
||||||
subscriptionCreateOptions.AutomaticTax ??= new SubscriptionAutomaticTaxOptions
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||||
{
|
{
|
||||||
Enabled = true
|
Enabled = customer.Address.Country == "US" ||
|
||||||
|
customer.TaxIds.Any()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +182,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
|||||||
await _mailService.SendProviderUpdatePaymentMethod(
|
await _mailService.SendProviderUpdatePaymentMethod(
|
||||||
organization.Id,
|
organization.Id,
|
||||||
organization.Name,
|
organization.Name,
|
||||||
provider.Name,
|
provider.Name!,
|
||||||
organizationOwnerEmails);
|
organizationOwnerEmails);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,13 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -53,6 +54,7 @@ public class ProviderService : IProviderService
|
|||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IProviderBillingService _providerBillingService;
|
private readonly IProviderBillingService _providerBillingService;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
|
private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand;
|
||||||
|
|
||||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
||||||
@ -61,7 +63,8 @@ public class ProviderService : IProviderService
|
|||||||
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
||||||
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
|
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
|
||||||
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
|
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
|
||||||
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient)
|
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient,
|
||||||
|
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand)
|
||||||
{
|
{
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
@ -81,6 +84,7 @@ public class ProviderService : IProviderService
|
|||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_providerBillingService = providerBillingService;
|
_providerBillingService = providerBillingService;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
|
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
|
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
|
||||||
@ -560,12 +564,12 @@ public class ProviderService : IProviderService
|
|||||||
|
|
||||||
ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan);
|
ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan);
|
||||||
|
|
||||||
var (organization, _, defaultCollection) = await _organizationService.SignupClientAsync(organizationSignup);
|
var signUpResponse = await _providerClientOrganizationSignUpCommand.SignUpClientOrganizationAsync(organizationSignup);
|
||||||
|
|
||||||
var providerOrganization = new ProviderOrganization
|
var providerOrganization = new ProviderOrganization
|
||||||
{
|
{
|
||||||
ProviderId = providerId,
|
ProviderId = providerId,
|
||||||
OrganizationId = organization.Id,
|
OrganizationId = signUpResponse.Organization.Id,
|
||||||
Key = organizationSignup.OwnerKey,
|
Key = organizationSignup.OwnerKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -574,12 +578,12 @@ public class ProviderService : IProviderService
|
|||||||
|
|
||||||
// Give the owner Can Manage access over the default collection
|
// Give the owner Can Manage access over the default collection
|
||||||
// The orgUser is not available when the org is created so we have to do it here as part of the invite
|
// The orgUser is not available when the org is created so we have to do it here as part of the invite
|
||||||
var defaultOwnerAccess = defaultCollection != null
|
var defaultOwnerAccess = signUpResponse.DefaultCollection != null
|
||||||
?
|
?
|
||||||
[
|
[
|
||||||
new CollectionAccessSelection
|
new CollectionAccessSelection
|
||||||
{
|
{
|
||||||
Id = defaultCollection.Id,
|
Id = signUpResponse.DefaultCollection.Id,
|
||||||
HidePasswords = false,
|
HidePasswords = false,
|
||||||
ReadOnly = false,
|
ReadOnly = false,
|
||||||
Manage = true
|
Manage = true
|
||||||
@ -587,7 +591,7 @@ public class ProviderService : IProviderService
|
|||||||
]
|
]
|
||||||
: Array.Empty<CollectionAccessSelection>();
|
: Array.Empty<CollectionAccessSelection>();
|
||||||
|
|
||||||
await _organizationService.InviteUsersAsync(organization.Id, user.Id, systemUser: null,
|
await _organizationService.InviteUsersAsync(signUpResponse.Organization.Id, user.Id, systemUser: null,
|
||||||
new (OrganizationUserInvite, string)[]
|
new (OrganizationUserInvite, string)[]
|
||||||
{
|
{
|
||||||
(
|
(
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using CsvHelper.Configuration.Attributes;
|
using CsvHelper.Configuration.Attributes;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Billing.Models;
|
namespace Bit.Commercial.Core.Billing.Providers.Models;
|
||||||
|
|
||||||
public class ProviderClientInvoiceReportRow
|
public class ProviderClientInvoiceReportRow
|
||||||
{
|
{
|
@ -7,11 +7,12 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing;
|
using Bit.Core.Billing;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -24,7 +25,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using OneOf;
|
using OneOf;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Billing;
|
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)]
|
[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)]
|
||||||
public class BusinessUnitConverter(
|
public class BusinessUnitConverter(
|
@ -1,5 +1,5 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Bit.Commercial.Core.Billing.Models;
|
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
@ -8,17 +8,17 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.Billing;
|
using Bit.Core.Billing;
|
||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Models;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Billing.Tax.Services;
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
@ -27,15 +27,13 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Braintree;
|
using Braintree;
|
||||||
using CsvHelper;
|
using CsvHelper;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
using static Bit.Core.Billing.Utilities;
|
using static Bit.Core.Billing.Utilities;
|
||||||
using Customer = Stripe.Customer;
|
using Customer = Stripe.Customer;
|
||||||
using Subscription = Stripe.Subscription;
|
using Subscription = Stripe.Subscription;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Billing;
|
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
|
|
||||||
public class ProviderBillingService(
|
public class ProviderBillingService(
|
||||||
IBraintreeGateway braintreeGateway,
|
IBraintreeGateway braintreeGateway,
|
||||||
@ -52,8 +50,7 @@ public class ProviderBillingService(
|
|||||||
ISetupIntentCache setupIntentCache,
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
ITaxService taxService,
|
ITaxService taxService)
|
||||||
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
|
|
||||||
: IProviderBillingService
|
: IProviderBillingService
|
||||||
{
|
{
|
||||||
public async Task AddExistingOrganization(
|
public async Task AddExistingOrganization(
|
||||||
@ -128,7 +125,7 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* We have to scale the provider's seats before the ProviderOrganization
|
* We have to scale the provider's seats before the ProviderOrganization
|
||||||
* row is inserted so the added organization's seats don't get double counted.
|
* row is inserted so the added organization's seats don't get double-counted.
|
||||||
*/
|
*/
|
||||||
await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value);
|
await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value);
|
||||||
|
|
||||||
@ -236,7 +233,7 @@ public class ProviderBillingService(
|
|||||||
|
|
||||||
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
|
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
|
||||||
{
|
{
|
||||||
Expand = ["tax_ids"]
|
Expand = ["tax", "tax_ids"]
|
||||||
});
|
});
|
||||||
|
|
||||||
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();
|
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();
|
||||||
@ -284,6 +281,13 @@ public class ProviderBillingService(
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge && providerCustomer.Address is not { Country: "US" })
|
||||||
|
{
|
||||||
|
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||||
|
}
|
||||||
|
|
||||||
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||||
|
|
||||||
organization.GatewayCustomerId = customer.Id;
|
organization.GatewayCustomerId = customer.Id;
|
||||||
@ -520,6 +524,13 @@ public class ProviderBillingService(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge && taxInfo.BillingAddressCountry != "US")
|
||||||
|
{
|
||||||
|
options.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
|
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
|
||||||
{
|
{
|
||||||
var taxIdType = taxService.GetStripeTaxCode(
|
var taxIdType = taxService.GetStripeTaxCode(
|
||||||
@ -531,6 +542,7 @@ public class ProviderBillingService(
|
|||||||
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
||||||
taxInfo.BillingAddressCountry,
|
taxInfo.BillingAddressCountry,
|
||||||
taxInfo.TaxIdNumber);
|
taxInfo.TaxIdNumber);
|
||||||
|
|
||||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -718,14 +730,21 @@ public class ProviderBillingService(
|
|||||||
TrialPeriodDays = trialPeriodDays
|
TrialPeriodDays = trialPeriodDays
|
||||||
};
|
};
|
||||||
|
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
var setNonUSBusinessUseToReverseCharge =
|
||||||
{
|
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
|
|
||||||
}
|
if (setNonUSBusinessUseToReverseCharge)
|
||||||
else
|
|
||||||
{
|
{
|
||||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
}
|
}
|
||||||
|
else if (customer.HasRecognizedTaxLocation())
|
||||||
|
{
|
||||||
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||||
|
{
|
||||||
|
Enabled = customer.Address.Country == "US" ||
|
||||||
|
customer.TaxIds.Any()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
@ -6,7 +6,7 @@ using Bit.Core.Billing;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Billing;
|
namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
|
|
||||||
public static class ProviderPriceAdapter
|
public static class ProviderPriceAdapter
|
||||||
{
|
{
|
@ -1,9 +1,9 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
using Bit.Commercial.Core.AdminConsole.Services;
|
using Bit.Commercial.Core.AdminConsole.Services;
|
||||||
using Bit.Commercial.Core.Billing;
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Utilities;
|
namespace Bit.Commercial.Core.Utilities;
|
||||||
|
107
bitwarden_license/src/Sso/package-lock.json
generated
107
bitwarden_license/src/Sso/package-lock.json
generated
@ -18,8 +18,8 @@
|
|||||||
"expose-loader": "5.0.1",
|
"expose-loader": "5.0.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"sass": "1.85.0",
|
"sass": "1.85.0",
|
||||||
"sass-loader": "16.0.4",
|
"sass-loader": "16.0.5",
|
||||||
"webpack": "5.97.1",
|
"webpack": "5.99.8",
|
||||||
"webpack-cli": "5.1.4"
|
"webpack-cli": "5.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1106,13 +1106,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-json-stable-stringify": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||||
@ -1754,16 +1747,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/punycode": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/randombytes": {
|
"node_modules/randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
@ -1898,9 +1881,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass-loader": {
|
"node_modules/sass-loader": {
|
||||||
"version": "16.0.4",
|
"version": "16.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
|
||||||
"integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==",
|
"integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1939,9 +1922,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/schema-utils": {
|
"node_modules/schema-utils": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||||
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
|
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2193,16 +2176,6 @@
|
|||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uri-js": {
|
|
||||||
"version": "4.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"punycode": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@ -2225,14 +2198,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.97.1",
|
"version": "5.99.8",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
|
||||||
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
|
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.6",
|
"@types/estree": "^1.0.6",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
"@webassemblyjs/ast": "^1.14.1",
|
"@webassemblyjs/ast": "^1.14.1",
|
||||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||||
@ -2249,9 +2223,9 @@
|
|||||||
"loader-runner": "^4.2.0",
|
"loader-runner": "^4.2.0",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"neo-async": "^2.6.2",
|
"neo-async": "^2.6.2",
|
||||||
"schema-utils": "^3.2.0",
|
"schema-utils": "^4.3.2",
|
||||||
"tapable": "^2.1.1",
|
"tapable": "^2.1.1",
|
||||||
"terser-webpack-plugin": "^5.3.10",
|
"terser-webpack-plugin": "^5.3.11",
|
||||||
"watchpack": "^2.4.1",
|
"watchpack": "^2.4.1",
|
||||||
"webpack-sources": "^3.2.3"
|
"webpack-sources": "^3.2.3"
|
||||||
},
|
},
|
||||||
@ -2352,59 +2326,6 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack/node_modules/ajv": {
|
|
||||||
"version": "6.12.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fast-deep-equal": "^3.1.1",
|
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
|
||||||
"json-schema-traverse": "^0.4.1",
|
|
||||||
"uri-js": "^4.2.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webpack/node_modules/ajv-keywords": {
|
|
||||||
"version": "3.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
|
||||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"ajv": "^6.9.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webpack/node_modules/json-schema-traverse": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/webpack/node_modules/schema-utils": {
|
|
||||||
"version": "3.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
|
||||||
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.8",
|
|
||||||
"ajv": "^6.12.5",
|
|
||||||
"ajv-keywords": "^3.5.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.13.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/webpack"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
"expose-loader": "5.0.1",
|
"expose-loader": "5.0.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"sass": "1.85.0",
|
"sass": "1.85.0",
|
||||||
"sass-loader": "16.0.4",
|
"sass-loader": "16.0.5",
|
||||||
"webpack": "5.97.1",
|
"webpack": "5.99.8",
|
||||||
"webpack-cli": "5.1.4"
|
"webpack-cli": "5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -7,8 +8,8 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Tax.Services;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -224,31 +225,115 @@ public class RemoveOrganizationFromProviderCommandTests
|
|||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
|
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
|
||||||
|
options.Description == string.Empty &&
|
||||||
|
options.Email == organization.BillingEmail &&
|
||||||
|
options.Expand[0] == "tax" &&
|
||||||
|
options.Expand[1] == "tax_ids")).Returns(new Customer
|
||||||
|
{
|
||||||
|
Id = "customer_id",
|
||||||
|
Address = new Address
|
||||||
|
{
|
||||||
|
Country = "US"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||||
{
|
{
|
||||||
Id = "subscription_id"
|
Id = "subscription_id"
|
||||||
});
|
});
|
||||||
|
|
||||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||||
.When(x => x.SetCreateOptions(
|
|
||||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||||
options.Customer == organization.GatewayCustomerId &&
|
options.Customer == organization.GatewayCustomerId &&
|
||||||
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
||||||
options.DaysUntilDue == 30 &&
|
options.DaysUntilDue == 30 &&
|
||||||
options.Metadata["organizationId"] == organization.Id.ToString() &&
|
options.AutomaticTax.Enabled == true &&
|
||||||
options.OffSession == true &&
|
options.Metadata["organizationId"] == organization.Id.ToString() &&
|
||||||
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
|
options.OffSession == true &&
|
||||||
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
|
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
|
||||||
options.Items.First().Quantity == organization.Seats)
|
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
|
||||||
, Arg.Any<Customer>()))
|
options.Items.First().Quantity == organization.Seats));
|
||||||
.Do(x =>
|
|
||||||
|
await sutProvider.GetDependency<IProviderBillingService>().Received(1)
|
||||||
|
.ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0);
|
||||||
|
|
||||||
|
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||||
|
org =>
|
||||||
|
org.BillingEmail == "a@example.com" &&
|
||||||
|
org.GatewaySubscriptionId == "subscription_id" &&
|
||||||
|
org.Status == OrganizationStatusType.Created));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
|
||||||
|
.DeleteAsync(providerOrganization);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||||
|
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
|
.SendProviderUpdatePaymentMethod(
|
||||||
|
organization.Id,
|
||||||
|
organization.Name,
|
||||||
|
provider.Name,
|
||||||
|
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_ConsolidatedBilling_ReverseCharge_MakesCorrectInvocations(
|
||||||
|
Provider provider,
|
||||||
|
ProviderOrganization providerOrganization,
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Status = ProviderStatusType.Billable;
|
||||||
|
|
||||||
|
providerOrganization.ProviderId = provider.Id;
|
||||||
|
|
||||||
|
organization.Status = OrganizationStatusType.Managed;
|
||||||
|
|
||||||
|
organization.PlanType = PlanType.TeamsMonthly;
|
||||||
|
|
||||||
|
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
|
||||||
|
providerOrganization.OrganizationId,
|
||||||
|
[],
|
||||||
|
includeProvider: false)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
|
||||||
|
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([
|
||||||
|
"a@example.com",
|
||||||
|
"b@example.com"
|
||||||
|
]);
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
|
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
|
||||||
|
options.Description == string.Empty &&
|
||||||
|
options.Email == organization.BillingEmail &&
|
||||||
|
options.Expand[0] == "tax" &&
|
||||||
|
options.Expand[1] == "tax_ids")).Returns(new Customer
|
||||||
{
|
{
|
||||||
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
|
Id = "customer_id",
|
||||||
|
Address = new Address
|
||||||
{
|
{
|
||||||
Enabled = true
|
Country = "US"
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
|
||||||
|
{
|
||||||
|
Id = "subscription_id"
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||||
|
|
||||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||||
|
|
||||||
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||||
|
@ -6,11 +6,12 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -717,8 +718,8 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
|
||||||
|
|
||||||
var providerOrganization =
|
var providerOrganization =
|
||||||
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||||
@ -755,8 +756,8 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BadRequestException>(() =>
|
await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
|
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
|
||||||
@ -782,8 +783,8 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, new Collection()));
|
.Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
|
||||||
|
|
||||||
var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||||
|
|
||||||
@ -821,8 +822,8 @@ public class ProviderServiceTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
|
sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
|
||||||
.Returns((organization, null as OrganizationUser, defaultCollection));
|
.Returns(new ProviderClientOrganizationSignUpResponse(organization, defaultCollection));
|
||||||
|
|
||||||
var providerOrganization =
|
var providerOrganization =
|
||||||
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Bit.Commercial.Core.Billing;
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing;
|
using Bit.Core.Billing;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -25,7 +25,7 @@ using NSubstitute;
|
|||||||
using Stripe;
|
using Stripe;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.Billing;
|
namespace Bit.Commercial.Core.Test.Billing.Providers;
|
||||||
|
|
||||||
public class BusinessUnitConverterTests
|
public class BusinessUnitConverterTests
|
||||||
{
|
{
|
@ -1,7 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Bit.Commercial.Core.Billing;
|
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||||
using Bit.Commercial.Core.Billing.Models;
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
@ -10,13 +10,13 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Models;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
|
||||||
using Bit.Core.Billing.Tax.Services;
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -40,7 +40,7 @@ using Customer = Stripe.Customer;
|
|||||||
using PaymentMethod = Stripe.PaymentMethod;
|
using PaymentMethod = Stripe.PaymentMethod;
|
||||||
using Subscription = Stripe.Subscription;
|
using Subscription = Stripe.Subscription;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.Billing;
|
namespace Bit.Commercial.Core.Test.Billing.Providers;
|
||||||
|
|
||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class ProviderBillingServiceTests
|
public class ProviderBillingServiceTests
|
||||||
@ -262,7 +262,7 @@ public class ProviderBillingServiceTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
|
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
|
||||||
options => options.Expand.FirstOrDefault() == "tax_ids"))
|
options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids")))
|
||||||
.Returns(providerCustomer);
|
.Returns(providerCustomer);
|
||||||
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
|
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
|
||||||
@ -312,6 +312,91 @@ public class ProviderBillingServiceTests
|
|||||||
org => org.GatewayCustomerId == "customer_id"));
|
org => org.GatewayCustomerId == "customer_id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateCustomer_ForClientOrg_ReverseCharge_Succeeds(
|
||||||
|
Provider provider,
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
organization.GatewayCustomerId = null;
|
||||||
|
organization.Name = "Name";
|
||||||
|
organization.BusinessName = "BusinessName";
|
||||||
|
|
||||||
|
var providerCustomer = new Customer
|
||||||
|
{
|
||||||
|
Address = new Address
|
||||||
|
{
|
||||||
|
Country = "CA",
|
||||||
|
PostalCode = "12345",
|
||||||
|
Line1 = "123 Main St.",
|
||||||
|
Line2 = "Unit 4",
|
||||||
|
City = "Fake Town",
|
||||||
|
State = "Fake State"
|
||||||
|
},
|
||||||
|
TaxIds = new StripeList<TaxId>
|
||||||
|
{
|
||||||
|
Data =
|
||||||
|
[
|
||||||
|
new TaxId { Type = "TYPE", Value = "VALUE" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
|
||||||
|
options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids")))
|
||||||
|
.Returns(providerCustomer);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
|
||||||
|
.Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings())
|
||||||
|
{
|
||||||
|
CloudRegion = "US"
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
||||||
|
options =>
|
||||||
|
options.Address.Country == providerCustomer.Address.Country &&
|
||||||
|
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
|
||||||
|
options.Address.Line1 == providerCustomer.Address.Line1 &&
|
||||||
|
options.Address.Line2 == providerCustomer.Address.Line2 &&
|
||||||
|
options.Address.City == providerCustomer.Address.City &&
|
||||||
|
options.Address.State == providerCustomer.Address.State &&
|
||||||
|
options.Name == organization.DisplayName() &&
|
||||||
|
options.Description == $"{provider.Name} Client Organization" &&
|
||||||
|
options.Email == provider.BillingEmail &&
|
||||||
|
options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" &&
|
||||||
|
options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" &&
|
||||||
|
options.Metadata["region"] == "US" &&
|
||||||
|
options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&
|
||||||
|
options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value &&
|
||||||
|
options.TaxExempt == StripeConstants.TaxExempt.Reverse))
|
||||||
|
.Returns(new Customer { Id = "customer_id" });
|
||||||
|
|
||||||
|
await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
|
||||||
|
options =>
|
||||||
|
options.Address.Country == providerCustomer.Address.Country &&
|
||||||
|
options.Address.PostalCode == providerCustomer.Address.PostalCode &&
|
||||||
|
options.Address.Line1 == providerCustomer.Address.Line1 &&
|
||||||
|
options.Address.Line2 == providerCustomer.Address.Line2 &&
|
||||||
|
options.Address.City == providerCustomer.Address.City &&
|
||||||
|
options.Address.State == providerCustomer.Address.State &&
|
||||||
|
options.Name == organization.DisplayName() &&
|
||||||
|
options.Description == $"{provider.Name} Client Organization" &&
|
||||||
|
options.Email == provider.BillingEmail &&
|
||||||
|
options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" &&
|
||||||
|
options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" &&
|
||||||
|
options.Metadata["region"] == "US" &&
|
||||||
|
options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&
|
||||||
|
options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
|
||||||
|
org => org.GatewayCustomerId == "customer_id"));
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region GenerateClientInvoiceReport
|
#region GenerateClientInvoiceReport
|
||||||
@ -1182,6 +1267,62 @@ public class ProviderBillingServiceTests
|
|||||||
Assert.Equivalent(expected, actual);
|
Assert.Equivalent(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupCustomer_WithCard_ReverseCharge_Success(
|
||||||
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
|
Provider provider,
|
||||||
|
TaxInfo taxInfo)
|
||||||
|
{
|
||||||
|
provider.Name = "MSP";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ITaxService>()
|
||||||
|
.GetStripeTaxCode(Arg.Is<string>(
|
||||||
|
p => p == taxInfo.BillingAddressCountry),
|
||||||
|
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||||
|
.Returns(taxInfo.TaxIdType);
|
||||||
|
|
||||||
|
taxInfo.BillingAddressCountry = "AD";
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
|
var expected = new Customer
|
||||||
|
{
|
||||||
|
Id = "customer_id",
|
||||||
|
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||||
|
|
||||||
|
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
|
||||||
|
o.Address.Country == taxInfo.BillingAddressCountry &&
|
||||||
|
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
||||||
|
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
||||||
|
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
||||||
|
o.Address.City == taxInfo.BillingAddressCity &&
|
||||||
|
o.Address.State == taxInfo.BillingAddressState &&
|
||||||
|
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
|
||||||
|
o.Email == provider.BillingEmail &&
|
||||||
|
o.PaymentMethod == tokenizedPaymentSource.Token &&
|
||||||
|
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token &&
|
||||||
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
|
||||||
|
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
|
||||||
|
o.Metadata["region"] == "" &&
|
||||||
|
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
|
||||||
|
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber &&
|
||||||
|
o.TaxExempt == StripeConstants.TaxExempt.Reverse))
|
||||||
|
.Returns(expected);
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
|
||||||
|
|
||||||
|
Assert.Equivalent(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
||||||
SutProvider<ProviderBillingService> sutProvider,
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
@ -1307,7 +1448,7 @@ public class ProviderBillingServiceTests
|
|||||||
.Returns(new Customer
|
.Returns(new Customer
|
||||||
{
|
{
|
||||||
Id = "customer_id",
|
Id = "customer_id",
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
Address = new Address { Country = "US" }
|
||||||
});
|
});
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
@ -1359,7 +1500,7 @@ public class ProviderBillingServiceTests
|
|||||||
var customer = new Customer
|
var customer = new Customer
|
||||||
{
|
{
|
||||||
Id = "customer_id",
|
Id = "customer_id",
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
Address = new Address { Country = "US" }
|
||||||
};
|
};
|
||||||
sutProvider.GetDependency<ISubscriberService>()
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
.GetCustomerOrThrow(
|
.GetCustomerOrThrow(
|
||||||
@ -1399,19 +1540,6 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
|
||||||
.When(x => x.SetCreateOptions(
|
|
||||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
|
||||||
options.Customer == "customer_id")
|
|
||||||
, Arg.Is<Customer>(p => p == customer)))
|
|
||||||
.Do(x =>
|
|
||||||
{
|
|
||||||
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
|
|
||||||
{
|
|
||||||
Enabled = true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||||
sub =>
|
sub =>
|
||||||
sub.AutomaticTax.Enabled == true &&
|
sub.AutomaticTax.Enabled == true &&
|
||||||
@ -1443,11 +1571,11 @@ public class ProviderBillingServiceTests
|
|||||||
var customer = new Customer
|
var customer = new Customer
|
||||||
{
|
{
|
||||||
Id = "customer_id",
|
Id = "customer_id",
|
||||||
|
Address = new Address { Country = "US" },
|
||||||
InvoiceSettings = new CustomerInvoiceSettings
|
InvoiceSettings = new CustomerInvoiceSettings
|
||||||
{
|
{
|
||||||
DefaultPaymentMethodId = "pm_123"
|
DefaultPaymentMethodId = "pm_123"
|
||||||
},
|
}
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<ISubscriberService>()
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
@ -1488,19 +1616,6 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
|
||||||
.When(x => x.SetCreateOptions(
|
|
||||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
|
||||||
options.Customer == "customer_id")
|
|
||||||
, Arg.Is<Customer>(p => p == customer)))
|
|
||||||
.Do(x =>
|
|
||||||
{
|
|
||||||
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
|
|
||||||
{
|
|
||||||
Enabled = true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
@ -1536,9 +1651,9 @@ public class ProviderBillingServiceTests
|
|||||||
var customer = new Customer
|
var customer = new Customer
|
||||||
{
|
{
|
||||||
Id = "customer_id",
|
Id = "customer_id",
|
||||||
|
Address = new Address { Country = "US" },
|
||||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||||
Metadata = new Dictionary<string, string>(),
|
Metadata = new Dictionary<string, string>()
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<ISubscriberService>()
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
@ -1579,19 +1694,6 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
|
||||||
.When(x => x.SetCreateOptions(
|
|
||||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
|
||||||
options.Customer == "customer_id")
|
|
||||||
, Arg.Is<Customer>(p => p == customer)))
|
|
||||||
.Do(x =>
|
|
||||||
{
|
|
||||||
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
|
|
||||||
{
|
|
||||||
Enabled = true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
@ -1646,12 +1748,15 @@ public class ProviderBillingServiceTests
|
|||||||
var customer = new Customer
|
var customer = new Customer
|
||||||
{
|
{
|
||||||
Id = "customer_id",
|
Id = "customer_id",
|
||||||
|
Address = new Address
|
||||||
|
{
|
||||||
|
Country = "US"
|
||||||
|
},
|
||||||
InvoiceSettings = new CustomerInvoiceSettings(),
|
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["btCustomerId"] = "braintree_customer_id"
|
["btCustomerId"] = "braintree_customer_id"
|
||||||
},
|
}
|
||||||
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<ISubscriberService>()
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
@ -1692,22 +1797,92 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IAutomaticTaxStrategy>()
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
.When(x => x.SetCreateOptions(
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
Arg.Is<SubscriptionCreateOptions>(options =>
|
|
||||||
options.Customer == "customer_id")
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||||
, Arg.Is<Customer>(p => p == customer)))
|
sub =>
|
||||||
.Do(x =>
|
sub.AutomaticTax.Enabled == true &&
|
||||||
|
sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&
|
||||||
|
sub.Customer == "customer_id" &&
|
||||||
|
sub.DaysUntilDue == null &&
|
||||||
|
sub.Items.Count == 2 &&
|
||||||
|
sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&
|
||||||
|
sub.Items.ElementAt(0).Quantity == 100 &&
|
||||||
|
sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&
|
||||||
|
sub.Items.ElementAt(1).Quantity == 100 &&
|
||||||
|
sub.Metadata["providerId"] == provider.Id.ToString() &&
|
||||||
|
sub.OffSession == true &&
|
||||||
|
sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
|
||||||
|
sub.TrialPeriodDays == 14)).Returns(expected);
|
||||||
|
|
||||||
|
var actual = await sutProvider.Sut.SetupSubscription(provider);
|
||||||
|
|
||||||
|
Assert.Equivalent(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SetupSubscription_ReverseCharge_Succeeds(
|
||||||
|
SutProvider<ProviderBillingService> sutProvider,
|
||||||
|
Provider provider)
|
||||||
|
{
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
provider.GatewaySubscriptionId = null;
|
||||||
|
|
||||||
|
var customer = new Customer
|
||||||
|
{
|
||||||
|
Id = "customer_id",
|
||||||
|
Address = new Address { Country = "CA" },
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettings
|
||||||
{
|
{
|
||||||
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
|
DefaultPaymentMethodId = "pm_123"
|
||||||
{
|
}
|
||||||
Enabled = true
|
};
|
||||||
};
|
|
||||||
});
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetCustomerOrThrow(
|
||||||
|
provider,
|
||||||
|
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer);
|
||||||
|
|
||||||
|
var providerPlans = new List<ProviderPlan>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
PlanType = PlanType.TeamsMonthly,
|
||||||
|
SeatMinimum = 100,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
PlanType = PlanType.EnterpriseMonthly,
|
||||||
|
SeatMinimum = 100,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var plan in providerPlans)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||||
|
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||||
|
}
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||||
|
.Returns(providerPlans);
|
||||||
|
|
||||||
|
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
|
||||||
sub =>
|
sub =>
|
||||||
sub.AutomaticTax.Enabled == true &&
|
sub.AutomaticTax.Enabled == true &&
|
@ -1,11 +1,11 @@
|
|||||||
using Bit.Commercial.Core.Billing;
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.Billing;
|
namespace Bit.Commercial.Core.Test.Billing.Providers;
|
||||||
|
|
||||||
public class ProviderPriceAdapterTests
|
public class ProviderPriceAdapterTests
|
||||||
{
|
{
|
@ -3,7 +3,7 @@ using Bit.Test.Common.AutoFixture;
|
|||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.Test.Billing;
|
namespace Bit.Commercial.Core.Test.Billing.Tax;
|
||||||
|
|
||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class TaxServiceTests
|
public class TaxServiceTests
|
@ -11,7 +11,7 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||||
|
@ -10,13 +10,13 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Models;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.SharedWeb.Utilities;
|
using Bit.SharedWeb.Utilities;
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
|
||||||
namespace Bit.Admin.AdminConsole.Models;
|
namespace Bit.Admin.AdminConsole.Models;
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ using Bit.Core.AdminConsole.Entities;
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Admin.Billing.Models;
|
using Bit.Admin.Billing.Models;
|
||||||
using Bit.Admin.Enums;
|
using Bit.Admin.Enums;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
using Bit.Core.Billing.Migration.Models;
|
using Bit.Core.Billing.Providers.Migration.Models;
|
||||||
using Bit.Core.Billing.Migration.Services;
|
using Bit.Core.Billing.Providers.Migration.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;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
|
||||||
namespace Bit.Admin.Billing.Models;
|
namespace Bit.Admin.Billing.Models;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult
|
@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Results";
|
ViewData["Title"] = "Results";
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult[]
|
@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult[]
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Results";
|
ViewData["Title"] = "Results";
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
|
|||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Migration;
|
using Bit.Core.Billing.Providers.Migration;
|
||||||
|
|
||||||
#if !OSS
|
#if !OSS
|
||||||
using Bit.Commercial.Core.Utilities;
|
using Bit.Commercial.Core.Utilities;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Jobs;
|
using Bit.Core.Jobs;
|
||||||
using Bit.Core.Tools.Repositories;
|
using Bit.Core.Tools.Repositories;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
|
||||||
namespace Bit.Admin.Tools.Jobs;
|
namespace Bit.Admin.Tools.Jobs;
|
||||||
@ -32,10 +32,10 @@ public class DeleteSendsJob : BaseJob
|
|||||||
}
|
}
|
||||||
using (var scope = _serviceProvider.CreateScope())
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
{
|
{
|
||||||
var sendService = scope.ServiceProvider.GetRequiredService<ISendService>();
|
var nonAnonymousSendCommand = scope.ServiceProvider.GetRequiredService<INonAnonymousSendCommand>();
|
||||||
foreach (var send in sends)
|
foreach (var send in sends)
|
||||||
{
|
{
|
||||||
await sendService.DeleteSendAsync(send);
|
await nonAnonymousSendCommand.DeleteSendAsync(send);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
107
src/Admin/package-lock.json
generated
107
src/Admin/package-lock.json
generated
@ -19,8 +19,8 @@
|
|||||||
"expose-loader": "5.0.1",
|
"expose-loader": "5.0.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"sass": "1.85.0",
|
"sass": "1.85.0",
|
||||||
"sass-loader": "16.0.4",
|
"sass-loader": "16.0.5",
|
||||||
"webpack": "5.97.1",
|
"webpack": "5.99.8",
|
||||||
"webpack-cli": "5.1.4"
|
"webpack-cli": "5.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1107,13 +1107,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-json-stable-stringify": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||||
@ -1755,16 +1748,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/punycode": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/randombytes": {
|
"node_modules/randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
@ -1899,9 +1882,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass-loader": {
|
"node_modules/sass-loader": {
|
||||||
"version": "16.0.4",
|
"version": "16.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
|
||||||
"integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==",
|
"integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1940,9 +1923,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/schema-utils": {
|
"node_modules/schema-utils": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||||
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
|
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2202,16 +2185,6 @@
|
|||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uri-js": {
|
|
||||||
"version": "4.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"punycode": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@ -2234,14 +2207,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.97.1",
|
"version": "5.99.8",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
|
||||||
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
|
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.6",
|
"@types/estree": "^1.0.6",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
"@webassemblyjs/ast": "^1.14.1",
|
"@webassemblyjs/ast": "^1.14.1",
|
||||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||||
@ -2258,9 +2232,9 @@
|
|||||||
"loader-runner": "^4.2.0",
|
"loader-runner": "^4.2.0",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"neo-async": "^2.6.2",
|
"neo-async": "^2.6.2",
|
||||||
"schema-utils": "^3.2.0",
|
"schema-utils": "^4.3.2",
|
||||||
"tapable": "^2.1.1",
|
"tapable": "^2.1.1",
|
||||||
"terser-webpack-plugin": "^5.3.10",
|
"terser-webpack-plugin": "^5.3.11",
|
||||||
"watchpack": "^2.4.1",
|
"watchpack": "^2.4.1",
|
||||||
"webpack-sources": "^3.2.3"
|
"webpack-sources": "^3.2.3"
|
||||||
},
|
},
|
||||||
@ -2361,59 +2335,6 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack/node_modules/ajv": {
|
|
||||||
"version": "6.12.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fast-deep-equal": "^3.1.1",
|
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
|
||||||
"json-schema-traverse": "^0.4.1",
|
|
||||||
"uri-js": "^4.2.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webpack/node_modules/ajv-keywords": {
|
|
||||||
"version": "3.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
|
||||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"ajv": "^6.9.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webpack/node_modules/json-schema-traverse": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/webpack/node_modules/schema-utils": {
|
|
||||||
"version": "3.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
|
||||||
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.8",
|
|
||||||
"ajv": "^6.12.5",
|
|
||||||
"ajv-keywords": "^3.5.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.13.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/webpack"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
"expose-loader": "5.0.1",
|
"expose-loader": "5.0.1",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"sass": "1.85.0",
|
"sass": "1.85.0",
|
||||||
"sass-loader": "16.0.4",
|
"sass-loader": "16.0.5",
|
||||||
"webpack": "5.97.1",
|
"webpack": "5.99.8",
|
||||||
"webpack-cli": "5.1.4"
|
"webpack-cli": "5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,11 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -137,7 +135,6 @@ public class OrganizationDomainController : Controller
|
|||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("domain/sso/verified")]
|
[HttpPost("domain/sso/verified")]
|
||||||
[RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)]
|
|
||||||
public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(
|
public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(
|
||||||
[FromBody] OrganizationDomainSsoDetailsRequestModel model)
|
[FromBody] OrganizationDomainSsoDetailsRequestModel model)
|
||||||
{
|
{
|
||||||
|
@ -25,7 +25,7 @@ using Bit.Core.Auth.Services;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
@ -8,6 +8,7 @@ using Bit.Core;
|
|||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -301,8 +302,12 @@ public class OrganizationBillingController(
|
|||||||
Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine.");
|
Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine.");
|
||||||
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
||||||
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
|
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
|
||||||
await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation);
|
|
||||||
await organizationBillingService.Finalize(sale);
|
await organizationBillingService.Finalize(sale);
|
||||||
|
var updatedOrg = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
if (updatedOrg != null)
|
||||||
|
{
|
||||||
|
await organizationBillingService.UpdatePaymentMethod(updatedOrg, paymentSource, taxInformation);
|
||||||
|
}
|
||||||
|
|
||||||
return TypedResults.Ok();
|
return TypedResults.Ok();
|
||||||
}
|
}
|
||||||
|
@ -109,28 +109,6 @@ public class OrganizationsController(
|
|||||||
return license;
|
return license;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/payment")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PostPayment(Guid id, [FromBody] PaymentRequestModel model)
|
|
||||||
{
|
|
||||||
if (!await currentContext.EditPaymentMethods(id))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await organizationService.ReplacePaymentMethodAsync(id, model.PaymentToken,
|
|
||||||
model.PaymentMethodType.Value, new TaxInfo
|
|
||||||
{
|
|
||||||
BillingAddressLine1 = model.Line1,
|
|
||||||
BillingAddressLine2 = model.Line2,
|
|
||||||
BillingAddressState = model.State,
|
|
||||||
BillingAddressCity = model.City,
|
|
||||||
BillingAddressPostalCode = model.PostalCode,
|
|
||||||
BillingAddressCountry = model.Country,
|
|
||||||
TaxIdNumber = model.TaxId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id:guid}/upgrade")]
|
[HttpPost("{id:guid}/upgrade")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model)
|
public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Models;
|
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Models;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -148,13 +150,33 @@ public class ProviderBillingController(
|
|||||||
|
|
||||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||||
|
|
||||||
|
var getProviderPriceFromStripe = featureService.IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe);
|
||||||
|
|
||||||
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
|
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
|
||||||
{
|
{
|
||||||
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
||||||
|
|
||||||
|
decimal unitAmount;
|
||||||
|
|
||||||
|
if (getProviderPriceFromStripe)
|
||||||
|
{
|
||||||
|
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type);
|
||||||
|
var price = await stripeAdapter.PriceGetAsync(priceId);
|
||||||
|
|
||||||
|
unitAmount = price.UnitAmountDecimal.HasValue
|
||||||
|
? price.UnitAmountDecimal.Value / 100M
|
||||||
|
: plan.PasswordManager.ProviderPortalSeatPrice;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unitAmount = plan.PasswordManager.ProviderPortalSeatPrice;
|
||||||
|
}
|
||||||
|
|
||||||
return new ConfiguredProviderPlan(
|
return new ConfiguredProviderPlan(
|
||||||
providerPlan.Id,
|
providerPlan.Id,
|
||||||
providerPlan.ProviderId,
|
providerPlan.ProviderId,
|
||||||
plan,
|
plan,
|
||||||
|
unitAmount,
|
||||||
providerPlan.SeatMinimum ?? 0,
|
providerPlan.SeatMinimum ?? 0,
|
||||||
providerPlan.PurchasedSeats ?? 0,
|
providerPlan.PurchasedSeats ?? 0,
|
||||||
providerPlan.AllocatedSeats ?? 0);
|
providerPlan.AllocatedSeats ?? 0);
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
|
using Bit.Core.Billing.Providers.Models;
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ public record ProviderSubscriptionResponse(
|
|||||||
.Select(providerPlan =>
|
.Select(providerPlan =>
|
||||||
{
|
{
|
||||||
var plan = providerPlan.Plan;
|
var plan = providerPlan.Plan;
|
||||||
var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice;
|
var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * providerPlan.Price;
|
||||||
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
|
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
|
||||||
return new ProviderPlanResponse(
|
return new ProviderPlanResponse(
|
||||||
plan.Name,
|
plan.Name,
|
||||||
|
@ -12,17 +12,17 @@ namespace Bit.Api.KeyManagement.Validators;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>
|
public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>
|
||||||
{
|
{
|
||||||
private readonly ISendService _sendService;
|
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||||
private readonly ISendRepository _sendRepository;
|
private readonly ISendRepository _sendRepository;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Instantiates a new <see cref="SendRotationValidator"/>
|
/// Instantiates a new <see cref="SendRotationValidator"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sendService">Enables conversion of <see cref="SendWithIdRequestModel"/> to <see cref="Send"/></param>
|
/// <param name="sendAuthorizationService">Enables conversion of <see cref="SendWithIdRequestModel"/> to <see cref="Send"/></param>
|
||||||
/// <param name="sendRepository">Retrieves all user <see cref="Send"/>s</param>
|
/// <param name="sendRepository">Retrieves all user <see cref="Send"/>s</param>
|
||||||
public SendRotationValidator(ISendService sendService, ISendRepository sendRepository)
|
public SendRotationValidator(ISendAuthorizationService sendAuthorizationService, ISendRepository sendRepository)
|
||||||
{
|
{
|
||||||
_sendService = sendService;
|
_sendAuthorizationService = sendAuthorizationService;
|
||||||
_sendRepository = sendRepository;
|
_sendRepository = sendRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRe
|
|||||||
throw new BadRequestException("All existing sends must be included in the rotation.");
|
throw new BadRequestException("All existing sends must be included in the rotation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Add(send.ToSend(existing, _sendService));
|
result.Add(send.ToSend(existing, _sendAuthorizationService));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -35,6 +35,7 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Tools.ImportFeatures;
|
using Bit.Core.Tools.ImportFeatures;
|
||||||
using Bit.Core.Tools.ReportFeatures;
|
using Bit.Core.Tools.ReportFeatures;
|
||||||
using Bit.Core.Auth.Models.Api.Request;
|
using Bit.Core.Auth.Models.Api.Request;
|
||||||
|
using Bit.Core.Tools.SendFeatures;
|
||||||
|
|
||||||
#if !OSS
|
#if !OSS
|
||||||
using Bit.Commercial.Core.SecretsManager;
|
using Bit.Commercial.Core.SecretsManager;
|
||||||
@ -186,6 +187,7 @@ public class Startup
|
|||||||
services.AddPhishingDomainServices(globalSettings);
|
services.AddPhishingDomainServices(globalSettings);
|
||||||
|
|
||||||
services.AddBillingQueries();
|
services.AddBillingQueries();
|
||||||
|
services.AddSendServices();
|
||||||
|
|
||||||
// Authorization Handlers
|
// Authorization Handlers
|
||||||
services.AddAuthorizationHandlers();
|
services.AddAuthorizationHandlers();
|
||||||
|
@ -12,6 +12,8 @@ using Bit.Core.Settings;
|
|||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Tools.Models.Data;
|
||||||
using Bit.Core.Tools.Repositories;
|
using Bit.Core.Tools.Repositories;
|
||||||
|
using Bit.Core.Tools.SendFeatures;
|
||||||
|
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -25,8 +27,10 @@ public class SendsController : Controller
|
|||||||
{
|
{
|
||||||
private readonly ISendRepository _sendRepository;
|
private readonly ISendRepository _sendRepository;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly ISendService _sendService;
|
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||||
private readonly ISendFileStorageService _sendFileStorageService;
|
private readonly ISendFileStorageService _sendFileStorageService;
|
||||||
|
private readonly IAnonymousSendCommand _anonymousSendCommand;
|
||||||
|
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
|
||||||
private readonly ILogger<SendsController> _logger;
|
private readonly ILogger<SendsController> _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
@ -34,7 +38,9 @@ public class SendsController : Controller
|
|||||||
public SendsController(
|
public SendsController(
|
||||||
ISendRepository sendRepository,
|
ISendRepository sendRepository,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
ISendService sendService,
|
ISendAuthorizationService sendAuthorizationService,
|
||||||
|
IAnonymousSendCommand anonymousSendCommand,
|
||||||
|
INonAnonymousSendCommand nonAnonymousSendCommand,
|
||||||
ISendFileStorageService sendFileStorageService,
|
ISendFileStorageService sendFileStorageService,
|
||||||
ILogger<SendsController> logger,
|
ILogger<SendsController> logger,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
@ -42,13 +48,16 @@ public class SendsController : Controller
|
|||||||
{
|
{
|
||||||
_sendRepository = sendRepository;
|
_sendRepository = sendRepository;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_sendService = sendService;
|
_sendAuthorizationService = sendAuthorizationService;
|
||||||
|
_anonymousSendCommand = anonymousSendCommand;
|
||||||
|
_nonAnonymousSendCommand = nonAnonymousSendCommand;
|
||||||
_sendFileStorageService = sendFileStorageService;
|
_sendFileStorageService = sendFileStorageService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Anonymous endpoints
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("access/{id}")]
|
[HttpPost("access/{id}")]
|
||||||
public async Task<IActionResult> Access(string id, [FromBody] SendAccessRequestModel model)
|
public async Task<IActionResult> Access(string id, [FromBody] SendAccessRequestModel model)
|
||||||
@ -61,18 +70,19 @@ public class SendsController : Controller
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
var guid = new Guid(CoreHelpers.Base64UrlDecode(id));
|
var guid = new Guid(CoreHelpers.Base64UrlDecode(id));
|
||||||
var (send, passwordRequired, passwordInvalid) =
|
var send = await _sendRepository.GetByIdAsync(guid);
|
||||||
await _sendService.AccessAsync(guid, model.Password);
|
SendAccessResult sendAuthResult =
|
||||||
if (passwordRequired)
|
await _sendAuthorizationService.AccessAsync(send, model.Password);
|
||||||
|
if (sendAuthResult.Equals(SendAccessResult.PasswordRequired))
|
||||||
{
|
{
|
||||||
return new UnauthorizedResult();
|
return new UnauthorizedResult();
|
||||||
}
|
}
|
||||||
if (passwordInvalid)
|
if (sendAuthResult.Equals(SendAccessResult.PasswordInvalid))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("Invalid password.");
|
throw new BadRequestException("Invalid password.");
|
||||||
}
|
}
|
||||||
if (send == null)
|
if (sendAuthResult.Equals(SendAccessResult.Denied))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
@ -106,19 +116,19 @@ public class SendsController : Controller
|
|||||||
throw new BadRequestException("Could not locate send");
|
throw new BadRequestException("Could not locate send");
|
||||||
}
|
}
|
||||||
|
|
||||||
var (url, passwordRequired, passwordInvalid) = await _sendService.GetSendFileDownloadUrlAsync(send, fileId,
|
var (url, result) = await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId,
|
||||||
model.Password);
|
model.Password);
|
||||||
|
|
||||||
if (passwordRequired)
|
if (result.Equals(SendAccessResult.PasswordRequired))
|
||||||
{
|
{
|
||||||
return new UnauthorizedResult();
|
return new UnauthorizedResult();
|
||||||
}
|
}
|
||||||
if (passwordInvalid)
|
if (result.Equals(SendAccessResult.PasswordInvalid))
|
||||||
{
|
{
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("Invalid password.");
|
throw new BadRequestException("Invalid password.");
|
||||||
}
|
}
|
||||||
if (send == null)
|
if (result.Equals(SendAccessResult.Denied))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
@ -130,6 +140,45 @@ public class SendsController : Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("file/validate/azure")]
|
||||||
|
public async Task<ObjectResult> AzureValidateFile()
|
||||||
|
{
|
||||||
|
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1];
|
||||||
|
var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);
|
||||||
|
var send = await _sendRepository.GetByIdAsync(new Guid(sendId));
|
||||||
|
if (send == null)
|
||||||
|
{
|
||||||
|
if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService)
|
||||||
|
{
|
||||||
|
await azureSendFileStorageService.DeleteBlobAsync(blobName);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _nonAnonymousSendCommand.ConfirmFileSize(send);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Non-anonymous endpoints
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<SendResponseModel> Get(string id)
|
public async Task<SendResponseModel> Get(string id)
|
||||||
{
|
{
|
||||||
@ -157,8 +206,8 @@ public class SendsController : Controller
|
|||||||
{
|
{
|
||||||
model.ValidateCreation();
|
model.ValidateCreation();
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var send = model.ToSend(userId, _sendService);
|
var send = model.ToSend(userId, _sendAuthorizationService);
|
||||||
await _sendService.SaveSendAsync(send);
|
await _nonAnonymousSendCommand.SaveSendAsync(send);
|
||||||
return new SendResponseModel(send, _globalSettings);
|
return new SendResponseModel(send, _globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,15 +224,15 @@ public class SendsController : Controller
|
|||||||
throw new BadRequestException("Invalid content. File size hint is required.");
|
throw new BadRequestException("Invalid content. File size hint is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.FileLength.Value > SendService.MAX_FILE_SIZE)
|
if (model.FileLength.Value > Constants.FileSize501mb)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Max file size is {SendService.MAX_FILE_SIZE_READABLE}.");
|
throw new BadRequestException($"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
model.ValidateCreation();
|
model.ValidateCreation();
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var (send, data) = model.ToSend(userId, model.File.FileName, _sendService);
|
var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService);
|
||||||
var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value);
|
var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value);
|
||||||
return new SendFileUploadDataResponseModel
|
return new SendFileUploadDataResponseModel
|
||||||
{
|
{
|
||||||
Url = uploadUrl,
|
Url = uploadUrl,
|
||||||
@ -230,41 +279,7 @@ public class SendsController : Controller
|
|||||||
var send = await _sendRepository.GetByIdAsync(new Guid(id));
|
var send = await _sendRepository.GetByIdAsync(new Guid(id));
|
||||||
await Request.GetFileAsync(async (stream) =>
|
await Request.GetFileAsync(async (stream) =>
|
||||||
{
|
{
|
||||||
await _sendService.UploadFileToExistingSendAsync(stream, send);
|
await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpPost("file/validate/azure")]
|
|
||||||
public async Task<ObjectResult> AzureValidateFile()
|
|
||||||
{
|
|
||||||
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
|
|
||||||
{
|
|
||||||
{
|
|
||||||
"Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1];
|
|
||||||
var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);
|
|
||||||
var send = await _sendRepository.GetByIdAsync(new Guid(sendId));
|
|
||||||
if (send == null)
|
|
||||||
{
|
|
||||||
if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService)
|
|
||||||
{
|
|
||||||
await azureSendFileStorageService.DeleteBlobAsync(blobName);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _sendService.ValidateSendFile(send);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +294,7 @@ public class SendsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _sendService.SaveSendAsync(model.ToSend(send, _sendService));
|
await _nonAnonymousSendCommand.SaveSendAsync(model.ToSend(send, _sendAuthorizationService));
|
||||||
return new SendResponseModel(send, _globalSettings);
|
return new SendResponseModel(send, _globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,7 +309,7 @@ public class SendsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
send.Password = null;
|
send.Password = null;
|
||||||
await _sendService.SaveSendAsync(send);
|
await _nonAnonymousSendCommand.SaveSendAsync(send);
|
||||||
return new SendResponseModel(send, _globalSettings);
|
return new SendResponseModel(send, _globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,6 +323,8 @@ public class SendsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _sendService.DeleteSendAsync(send);
|
await _nonAnonymousSendCommand.DeleteSendAsync(send);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -36,31 +36,31 @@ public class SendRequestModel
|
|||||||
public bool? Disabled { get; set; }
|
public bool? Disabled { get; set; }
|
||||||
public bool? HideEmail { get; set; }
|
public bool? HideEmail { get; set; }
|
||||||
|
|
||||||
public Send ToSend(Guid userId, ISendService sendService)
|
public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService)
|
||||||
{
|
{
|
||||||
var send = new Send
|
var send = new Send
|
||||||
{
|
{
|
||||||
Type = Type,
|
Type = Type,
|
||||||
UserId = (Guid?)userId
|
UserId = (Guid?)userId
|
||||||
};
|
};
|
||||||
ToSend(send, sendService);
|
ToSend(send, sendAuthorizationService);
|
||||||
return send;
|
return send;
|
||||||
}
|
}
|
||||||
|
|
||||||
public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendService sendService)
|
public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendAuthorizationService sendAuthorizationService)
|
||||||
{
|
{
|
||||||
var send = ToSendBase(new Send
|
var send = ToSendBase(new Send
|
||||||
{
|
{
|
||||||
Type = Type,
|
Type = Type,
|
||||||
UserId = (Guid?)userId
|
UserId = (Guid?)userId
|
||||||
}, sendService);
|
}, sendAuthorizationService);
|
||||||
var data = new SendFileData(Name, Notes, fileName);
|
var data = new SendFileData(Name, Notes, fileName);
|
||||||
return (send, data);
|
return (send, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Send ToSend(Send existingSend, ISendService sendService)
|
public Send ToSend(Send existingSend, ISendAuthorizationService sendAuthorizationService)
|
||||||
{
|
{
|
||||||
existingSend = ToSendBase(existingSend, sendService);
|
existingSend = ToSendBase(existingSend, sendAuthorizationService);
|
||||||
switch (existingSend.Type)
|
switch (existingSend.Type)
|
||||||
{
|
{
|
||||||
case SendType.File:
|
case SendType.File:
|
||||||
@ -125,7 +125,7 @@ public class SendRequestModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Send ToSendBase(Send existingSend, ISendService sendService)
|
private Send ToSendBase(Send existingSend, ISendAuthorizationService authorizationService)
|
||||||
{
|
{
|
||||||
existingSend.Key = Key;
|
existingSend.Key = Key;
|
||||||
existingSend.ExpirationDate = ExpirationDate;
|
existingSend.ExpirationDate = ExpirationDate;
|
||||||
@ -133,7 +133,7 @@ public class SendRequestModel
|
|||||||
existingSend.MaxAccessCount = MaxAccessCount;
|
existingSend.MaxAccessCount = MaxAccessCount;
|
||||||
if (!string.IsNullOrWhiteSpace(Password))
|
if (!string.IsNullOrWhiteSpace(Password))
|
||||||
{
|
{
|
||||||
existingSend.Password = sendService.HashPassword(Password);
|
existingSend.Password = authorizationService.HashPassword(Password);
|
||||||
}
|
}
|
||||||
existingSend.Disabled = Disabled.GetValueOrDefault();
|
existingSend.Disabled = Disabled.GetValueOrDefault();
|
||||||
existingSend.HideEmail = HideEmail.GetValueOrDefault();
|
existingSend.HideEmail = HideEmail.GetValueOrDefault();
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Billing.Constants;
|
using Bit.Billing.Constants;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
|
||||||
using Bit.Core.Billing.Tax.Services;
|
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -25,8 +25,7 @@ public class UpcomingInvoiceHandler(
|
|||||||
IStripeEventService stripeEventService,
|
IStripeEventService stripeEventService,
|
||||||
IStripeEventUtilityService stripeEventUtilityService,
|
IStripeEventUtilityService stripeEventUtilityService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IValidateSponsorshipCommand validateSponsorshipCommand,
|
IValidateSponsorshipCommand validateSponsorshipCommand)
|
||||||
IAutomaticTaxFactory automaticTaxFactory)
|
|
||||||
: IUpcomingInvoiceHandler
|
: IUpcomingInvoiceHandler
|
||||||
{
|
{
|
||||||
public async Task HandleAsync(Event parsedEvent)
|
public async Task HandleAsync(Event parsedEvent)
|
||||||
@ -46,6 +45,8 @@ public class UpcomingInvoiceHandler(
|
|||||||
|
|
||||||
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
||||||
|
|
||||||
|
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
if (organizationId.HasValue)
|
if (organizationId.HasValue)
|
||||||
{
|
{
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId.Value);
|
var organization = await organizationRepository.GetByIdAsync(organizationId.Value);
|
||||||
@ -55,7 +56,7 @@ public class UpcomingInvoiceHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await TryEnableAutomaticTaxAsync(subscription);
|
await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||||
|
|
||||||
@ -100,7 +101,25 @@ public class UpcomingInvoiceHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await TryEnableAutomaticTaxAsync(subscription);
|
if (!subscription.AutomaticTax.Enabled && subscription.Customer.HasRecognizedTaxLocation())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await stripeFacade.UpdateSubscription(subscription.Id,
|
||||||
|
new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to set user's ({UserID}) subscription to automatic tax while processing event with ID {EventID}",
|
||||||
|
user.Id,
|
||||||
|
parsedEvent.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (user.Premium)
|
if (user.Premium)
|
||||||
{
|
{
|
||||||
@ -116,7 +135,7 @@ public class UpcomingInvoiceHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await TryEnableAutomaticTaxAsync(subscription);
|
await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
await SendUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice);
|
await SendUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice);
|
||||||
}
|
}
|
||||||
@ -139,50 +158,123 @@ public class UpcomingInvoiceHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TryEnableAutomaticTaxAsync(Subscription subscription)
|
private async Task AlignOrganizationTaxConcernsAsync(
|
||||||
|
Organization organization,
|
||||||
|
Subscription subscription,
|
||||||
|
string eventId,
|
||||||
|
bool setNonUSBusinessUseToReverseCharge)
|
||||||
{
|
{
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
var nonUSBusinessUse =
|
||||||
{
|
organization.PlanType.GetProductTier() != ProductTierType.Families &&
|
||||||
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscription.Items.Select(x => x.Price.Id));
|
subscription.Customer.Address.Country != "US";
|
||||||
var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters);
|
|
||||||
var updateOptions = automaticTaxStrategy.GetUpdateOptions(subscription);
|
|
||||||
|
|
||||||
if (updateOptions == null)
|
bool setAutomaticTaxToEnabled;
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge)
|
||||||
|
{
|
||||||
|
if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||||
{
|
{
|
||||||
return;
|
try
|
||||||
|
{
|
||||||
|
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||||
|
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to set organization's ({OrganizationID}) to reverse tax exemption while processing event with ID {EventID}",
|
||||||
|
organization.Id,
|
||||||
|
eventId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await stripeFacade.UpdateSubscription(subscription.Id, updateOptions);
|
setAutomaticTaxToEnabled = true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (subscription.AutomaticTax.Enabled ||
|
|
||||||
!subscription.Customer.HasBillingLocation() ||
|
|
||||||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
|
|
||||||
{
|
{
|
||||||
return;
|
setAutomaticTaxToEnabled =
|
||||||
|
subscription.Customer.HasRecognizedTaxLocation() &&
|
||||||
|
(subscription.Customer.Address.Country == "US" ||
|
||||||
|
(nonUSBusinessUse && subscription.Customer.TaxIds.Any()));
|
||||||
}
|
}
|
||||||
|
|
||||||
await stripeFacade.UpdateSubscription(subscription.Id,
|
if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled)
|
||||||
new SubscriptionUpdateOptions
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
DefaultTaxRates = [],
|
await stripeFacade.UpdateSubscription(subscription.Id,
|
||||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
new SubscriptionUpdateOptions
|
||||||
});
|
{
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to set organization's ({OrganizationID}) subscription to automatic tax while processing event with ID {EventID}",
|
||||||
|
organization.Id,
|
||||||
|
eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
private async Task AlignProviderTaxConcernsAsync(
|
||||||
|
Provider provider,
|
||||||
|
Subscription subscription,
|
||||||
|
string eventId,
|
||||||
|
bool setNonUSBusinessUseToReverseCharge)
|
||||||
|
{
|
||||||
|
bool setAutomaticTaxToEnabled;
|
||||||
|
|
||||||
async Task<bool> IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription)
|
if (setNonUSBusinessUseToReverseCharge)
|
||||||
{
|
{
|
||||||
var familyPriceIds = (await Task.WhenAll(
|
if (subscription.Customer.Address.Country != "US" && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||||
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
|
{
|
||||||
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
|
try
|
||||||
.Select(plan => plan.PasswordManager.StripePlanId);
|
{
|
||||||
|
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||||
|
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}",
|
||||||
|
provider.Id,
|
||||||
|
eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return localSubscription.Customer.Address.Country != "US" &&
|
setAutomaticTaxToEnabled = true;
|
||||||
localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
|
}
|
||||||
!localSubscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() &&
|
else
|
||||||
!localSubscription.Customer.TaxIds.Any();
|
{
|
||||||
|
setAutomaticTaxToEnabled =
|
||||||
|
subscription.Customer.HasRecognizedTaxLocation() &&
|
||||||
|
(subscription.Customer.Address.Country == "US" ||
|
||||||
|
subscription.Customer.TaxIds.Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await stripeFacade.UpdateSubscription(subscription.Id,
|
||||||
|
new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to set provider's ({ProviderID}) subscription to automatic tax while processing event with ID {EventID}",
|
||||||
|
provider.Id,
|
||||||
|
eventId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,187 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.StaticStore;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Tools.Enums;
|
||||||
|
using Bit.Core.Tools.Models.Business;
|
||||||
|
using Bit.Core.Tools.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||||
|
|
||||||
|
public record ProviderClientOrganizationSignUpResponse(
|
||||||
|
Organization Organization,
|
||||||
|
Collection DefaultCollection);
|
||||||
|
|
||||||
|
public interface IProviderClientOrganizationSignUpCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sign up a new client organization for a provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="signup">The signup information.</param>
|
||||||
|
/// <returns>A tuple containing the new organization and its default collection.</returns>
|
||||||
|
Task<ProviderClientOrganizationSignUpResponse> SignUpClientOrganizationAsync(OrganizationSignup signup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizationSignUpCommand
|
||||||
|
{
|
||||||
|
public const string PlanNullErrorMessage = "Password Manager Plan was null.";
|
||||||
|
public const string PlanDisabledErrorMessage = "Password Manager Plan is disabled.";
|
||||||
|
public const string AdditionalSeatsNegativeErrorMessage = "You can't subtract Password Manager seats!";
|
||||||
|
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IPricingClient _pricingClient;
|
||||||
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
|
private readonly ICollectionRepository _collectionRepository;
|
||||||
|
|
||||||
|
public ProviderClientOrganizationSignUpCommand(
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IPricingClient pricingClient,
|
||||||
|
IReferenceEventService referenceEventService,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
|
IApplicationCacheService applicationCacheService,
|
||||||
|
ICollectionRepository collectionRepository)
|
||||||
|
{
|
||||||
|
_currentContext = currentContext;
|
||||||
|
_pricingClient = pricingClient;
|
||||||
|
_referenceEventService = referenceEventService;
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
|
_applicationCacheService = applicationCacheService;
|
||||||
|
_collectionRepository = collectionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProviderClientOrganizationSignUpResponse> SignUpClientOrganizationAsync(OrganizationSignup signup)
|
||||||
|
{
|
||||||
|
var plan = await _pricingClient.GetPlanOrThrow(signup.Plan);
|
||||||
|
|
||||||
|
ValidatePlan(plan, signup.AdditionalSeats);
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
// Pre-generate the org id so that we can save it with the Stripe subscription.
|
||||||
|
Id = CoreHelpers.GenerateComb(),
|
||||||
|
Name = signup.Name,
|
||||||
|
BillingEmail = signup.BillingEmail,
|
||||||
|
PlanType = plan!.Type,
|
||||||
|
Seats = signup.AdditionalSeats,
|
||||||
|
MaxCollections = plan.PasswordManager.MaxCollections,
|
||||||
|
MaxStorageGb = 1,
|
||||||
|
UsePolicies = plan.HasPolicies,
|
||||||
|
UseSso = plan.HasSso,
|
||||||
|
UseOrganizationDomains = plan.HasOrganizationDomains,
|
||||||
|
UseGroups = plan.HasGroups,
|
||||||
|
UseEvents = plan.HasEvents,
|
||||||
|
UseDirectory = plan.HasDirectory,
|
||||||
|
UseTotp = plan.HasTotp,
|
||||||
|
Use2fa = plan.Has2fa,
|
||||||
|
UseApi = plan.HasApi,
|
||||||
|
UseResetPassword = plan.HasResetPassword,
|
||||||
|
SelfHost = plan.HasSelfHost,
|
||||||
|
UsersGetPremium = plan.UsersGetPremium,
|
||||||
|
UseCustomPermissions = plan.HasCustomPermissions,
|
||||||
|
UseScim = plan.HasScim,
|
||||||
|
Plan = plan.Name,
|
||||||
|
Gateway = GatewayType.Stripe,
|
||||||
|
ReferenceData = signup.Owner.ReferenceData,
|
||||||
|
Enabled = true,
|
||||||
|
LicenseKey = CoreHelpers.SecureRandomString(20),
|
||||||
|
PublicKey = signup.PublicKey,
|
||||||
|
PrivateKey = signup.PrivateKey,
|
||||||
|
CreationDate = DateTime.UtcNow,
|
||||||
|
RevisionDate = DateTime.UtcNow,
|
||||||
|
Status = OrganizationStatusType.Created,
|
||||||
|
UsePasswordManager = true,
|
||||||
|
// Secrets Manager not available for purchase with Consolidated Billing.
|
||||||
|
UseSecretsManager = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
var returnValue = await SignUpAsync(organization, signup.CollectionName);
|
||||||
|
|
||||||
|
await _referenceEventService.RaiseEventAsync(
|
||||||
|
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
|
||||||
|
{
|
||||||
|
PlanName = plan.Name,
|
||||||
|
PlanType = plan.Type,
|
||||||
|
Seats = returnValue.Organization.Seats,
|
||||||
|
SignupInitiationPath = signup.InitiationPath,
|
||||||
|
Storage = returnValue.Organization.MaxStorageGb,
|
||||||
|
});
|
||||||
|
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidatePlan(Plan plan, int additionalSeats)
|
||||||
|
{
|
||||||
|
if (plan is null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(PlanNullErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.Disabled)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(PlanDisabledErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalSeats < 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(AdditionalSeatsNegativeErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Private helper method to create a new organization.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<ProviderClientOrganizationSignUpResponse> SignUpAsync(
|
||||||
|
Organization organization, string collectionName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _organizationRepository.CreateAsync(organization);
|
||||||
|
await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
ApiKey = CoreHelpers.SecureRandomString(30),
|
||||||
|
Type = OrganizationApiKeyType.Default,
|
||||||
|
RevisionDate = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||||
|
|
||||||
|
Collection defaultCollection = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(collectionName))
|
||||||
|
{
|
||||||
|
defaultCollection = new Collection
|
||||||
|
{
|
||||||
|
Name = collectionName,
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
CreationDate = organization.CreationDate,
|
||||||
|
RevisionDate = organization.CreationDate
|
||||||
|
};
|
||||||
|
|
||||||
|
await _collectionRepository.CreateAsync(defaultCollection, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ProviderClientOrganizationSignUpResponse(organization, defaultCollection);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (organization.Id != default)
|
||||||
|
{
|
||||||
|
await _organizationRepository.DeleteAsync(organization);
|
||||||
|
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,8 +11,6 @@ namespace Bit.Core.Services;
|
|||||||
|
|
||||||
public interface IOrganizationService
|
public interface IOrganizationService
|
||||||
{
|
{
|
||||||
Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, PaymentMethodType paymentMethodType,
|
|
||||||
TaxInfo taxInfo);
|
|
||||||
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
|
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
|
||||||
Task ReinstateSubscriptionAsync(Guid organizationId);
|
Task ReinstateSubscriptionAsync(Guid organizationId);
|
||||||
Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);
|
Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);
|
||||||
@ -20,9 +18,6 @@ public interface IOrganizationService
|
|||||||
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd);
|
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd);
|
||||||
Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
|
Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
|
||||||
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
|
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
|
||||||
#nullable enable
|
|
||||||
Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup);
|
|
||||||
#nullable disable
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new organization on a self-hosted instance
|
/// Create a new organization on a self-hosted instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -144,27 +144,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
|
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
|
||||||
PaymentMethodType paymentMethodType, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var organization = await GetOrgById(organizationId);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _paymentService.SaveTaxInfoAsync(organization, taxInfo);
|
|
||||||
var updated = await _paymentService.UpdatePaymentMethodAsync(
|
|
||||||
organization,
|
|
||||||
paymentMethodType,
|
|
||||||
paymentToken,
|
|
||||||
taxInfo);
|
|
||||||
if (updated)
|
|
||||||
{
|
|
||||||
await ReplaceAndUpdateCacheAsync(organization);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null)
|
public async Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null)
|
||||||
{
|
{
|
||||||
var organization = await GetOrgById(organizationId);
|
var organization = await GetOrgById(organizationId);
|
||||||
@ -431,66 +410,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup)
|
|
||||||
{
|
|
||||||
var plan = await _pricingClient.GetPlanOrThrow(signup.Plan);
|
|
||||||
|
|
||||||
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
|
|
||||||
|
|
||||||
var organization = new Organization
|
|
||||||
{
|
|
||||||
// Pre-generate the org id so that we can save it with the Stripe subscription.
|
|
||||||
Id = CoreHelpers.GenerateComb(),
|
|
||||||
Name = signup.Name,
|
|
||||||
BillingEmail = signup.BillingEmail,
|
|
||||||
PlanType = plan!.Type,
|
|
||||||
Seats = signup.AdditionalSeats,
|
|
||||||
MaxCollections = plan.PasswordManager.MaxCollections,
|
|
||||||
MaxStorageGb = 1,
|
|
||||||
UsePolicies = plan.HasPolicies,
|
|
||||||
UseSso = plan.HasSso,
|
|
||||||
UseOrganizationDomains = plan.HasOrganizationDomains,
|
|
||||||
UseGroups = plan.HasGroups,
|
|
||||||
UseEvents = plan.HasEvents,
|
|
||||||
UseDirectory = plan.HasDirectory,
|
|
||||||
UseTotp = plan.HasTotp,
|
|
||||||
Use2fa = plan.Has2fa,
|
|
||||||
UseApi = plan.HasApi,
|
|
||||||
UseResetPassword = plan.HasResetPassword,
|
|
||||||
SelfHost = plan.HasSelfHost,
|
|
||||||
UsersGetPremium = plan.UsersGetPremium,
|
|
||||||
UseCustomPermissions = plan.HasCustomPermissions,
|
|
||||||
UseScim = plan.HasScim,
|
|
||||||
Plan = plan.Name,
|
|
||||||
Gateway = GatewayType.Stripe,
|
|
||||||
ReferenceData = signup.Owner.ReferenceData,
|
|
||||||
Enabled = true,
|
|
||||||
LicenseKey = CoreHelpers.SecureRandomString(20),
|
|
||||||
PublicKey = signup.PublicKey,
|
|
||||||
PrivateKey = signup.PrivateKey,
|
|
||||||
CreationDate = DateTime.UtcNow,
|
|
||||||
RevisionDate = DateTime.UtcNow,
|
|
||||||
Status = OrganizationStatusType.Created,
|
|
||||||
UsePasswordManager = true,
|
|
||||||
// Secrets Manager not available for purchase with Consolidated Billing.
|
|
||||||
UseSecretsManager = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
var returnValue = await SignUpAsync(organization, default, signup.OwnerKey, signup.CollectionName, false);
|
|
||||||
|
|
||||||
await _referenceEventService.RaiseEventAsync(
|
|
||||||
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
|
|
||||||
{
|
|
||||||
PlanName = plan.Name,
|
|
||||||
PlanType = plan.Type,
|
|
||||||
Seats = returnValue.Item1.Seats,
|
|
||||||
SignupInitiationPath = signup.InitiationPath,
|
|
||||||
Storage = returnValue.Item1.MaxStorageGb,
|
|
||||||
});
|
|
||||||
|
|
||||||
return returnValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
|
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
|
||||||
{
|
{
|
||||||
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
|
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
|
||||||
|
@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
public static class StripeConstants
|
public static class StripeConstants
|
||||||
{
|
{
|
||||||
public static class Prices
|
|
||||||
{
|
|
||||||
public const string StoragePlanPersonal = "personal-storage-gb-annually";
|
|
||||||
}
|
|
||||||
public static class AutomaticTaxStatus
|
public static class AutomaticTaxStatus
|
||||||
{
|
{
|
||||||
public const string Failed = "failed";
|
public const string Failed = "failed";
|
||||||
@ -69,6 +65,11 @@ public static class StripeConstants
|
|||||||
public const string USBankAccount = "us_bank_account";
|
public const string USBankAccount = "us_bank_account";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Prices
|
||||||
|
{
|
||||||
|
public const string StoragePlanPersonal = "personal-storage-gb-annually";
|
||||||
|
}
|
||||||
|
|
||||||
public static class ProrationBehavior
|
public static class ProrationBehavior
|
||||||
{
|
{
|
||||||
public const string AlwaysInvoice = "always_invoice";
|
public const string AlwaysInvoice = "always_invoice";
|
||||||
@ -88,6 +89,13 @@ public static class StripeConstants
|
|||||||
public const string Paused = "paused";
|
public const string Paused = "paused";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class TaxExempt
|
||||||
|
{
|
||||||
|
public const string Exempt = "exempt";
|
||||||
|
public const string None = "none";
|
||||||
|
public const string Reverse = "reverse";
|
||||||
|
}
|
||||||
|
|
||||||
public static class ValidateTaxLocationTiming
|
public static class ValidateTaxLocationTiming
|
||||||
{
|
{
|
||||||
public const string Deferred = "deferred";
|
public const string Deferred = "deferred";
|
||||||
|
@ -15,12 +15,7 @@ public static class CustomerExtensions
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
public static bool HasRecognizedTaxLocation(this Customer customer) =>
|
||||||
/// Determines if a Stripe customer supports automatic tax
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="customer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static bool HasTaxLocationVerified(this Customer customer) =>
|
|
||||||
customer?.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.UnrecognizedLocation;
|
customer?.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.UnrecognizedLocation;
|
||||||
|
|
||||||
public static decimal GetBillingBalance(this Customer customer)
|
public static decimal GetBillingBalance(this Customer customer)
|
||||||
|
@ -22,7 +22,7 @@ public static class SubscriptionUpdateOptionsExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We might only need to check the automatic tax status.
|
// We might only need to check the automatic tax status.
|
||||||
if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country))
|
if (!customer.HasRecognizedTaxLocation() && string.IsNullOrWhiteSpace(customer.Address?.Country))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ public static class UpcomingInvoiceOptionsExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We might only need to check the automatic tax status.
|
// We might only need to check the automatic tax status.
|
||||||
if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country))
|
if (!customer.HasRecognizedTaxLocation() && string.IsNullOrWhiteSpace(customer.Address?.Country))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
#nullable enable
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
#nullable enable
|
namespace Bit.Core.Billing.Providers.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Entities;
|
|
||||||
|
|
||||||
public class ClientOrganizationMigrationRecord : ITableObject<Guid>
|
public class ClientOrganizationMigrationRecord : ITableObject<Guid>
|
||||||
{
|
{
|
@ -1,10 +1,10 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
#nullable enable
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
#nullable enable
|
namespace Bit.Core.Billing.Providers.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Entities;
|
|
||||||
|
|
||||||
public class ProviderInvoiceItem : ITableObject<Guid>
|
public class ProviderInvoiceItem : ITableObject<Guid>
|
||||||
{
|
{
|
@ -1,10 +1,10 @@
|
|||||||
using Bit.Core.Billing.Enums;
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
#nullable enable
|
namespace Bit.Core.Billing.Providers.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Entities;
|
|
||||||
|
|
||||||
public class ProviderPlan : ITableObject<Guid>
|
public class ProviderPlan : ITableObject<Guid>
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.Billing.Migration.Models;
|
namespace Bit.Core.Billing.Providers.Migration.Models;
|
||||||
|
|
||||||
public enum ClientMigrationProgress
|
public enum ClientMigrationProgress
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Migration.Models;
|
namespace Bit.Core.Billing.Providers.Migration.Models;
|
||||||
|
|
||||||
public class ProviderMigrationResult
|
public class ProviderMigrationResult
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.Billing.Migration.Models;
|
namespace Bit.Core.Billing.Providers.Migration.Models;
|
||||||
|
|
||||||
public enum ProviderMigrationProgress
|
public enum ProviderMigrationProgress
|
||||||
{
|
{
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Core.Billing.Migration.Services;
|
using Bit.Core.Billing.Providers.Migration.Services;
|
||||||
using Bit.Core.Billing.Migration.Services.Implementations;
|
using Bit.Core.Billing.Providers.Migration.Services.Implementations;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Migration;
|
namespace Bit.Core.Billing.Providers.Migration;
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Billing.Migration.Models;
|
using Bit.Core.Billing.Providers.Migration.Models;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Migration.Services;
|
namespace Bit.Core.Billing.Providers.Migration.Services;
|
||||||
|
|
||||||
public interface IMigrationTrackerCache
|
public interface IMigrationTrackerCache
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Migration.Services;
|
namespace Bit.Core.Billing.Providers.Migration.Services;
|
||||||
|
|
||||||
public interface IOrganizationMigrator
|
public interface IOrganizationMigrator
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.Billing.Migration.Models;
|
using Bit.Core.Billing.Providers.Migration.Models;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Migration.Services;
|
namespace Bit.Core.Billing.Providers.Migration.Services;
|
||||||
|
|
||||||
public interface IProviderMigrator
|
public interface IProviderMigrator
|
||||||
{
|
{
|
@ -1,11 +1,11 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Billing.Migration.Models;
|
using Bit.Core.Billing.Providers.Migration.Models;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Migration.Services.Implementations;
|
namespace Bit.Core.Billing.Providers.Migration.Services.Implementations;
|
||||||
|
|
||||||
public class MigrationTrackerDistributedCache(
|
public class MigrationTrackerDistributedCache(
|
||||||
[FromKeyedServices("persistent")]
|
[FromKeyedServices("persistent")]
|
@ -1,10 +1,10 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Migration.Models;
|
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Migration.Models;
|
||||||
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Stripe;
|
using Stripe;
|
||||||
using Plan = Bit.Core.Models.StaticStore.Plan;
|
using Plan = Bit.Core.Models.StaticStore.Plan;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Migration.Services.Implementations;
|
namespace Bit.Core.Billing.Providers.Migration.Services.Implementations;
|
||||||
|
|
||||||
public class OrganizationMigrator(
|
public class OrganizationMigrator(
|
||||||
IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository,
|
IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository,
|
@ -3,18 +3,18 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Migration.Models;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Providers.Migration.Models;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Providers.Models;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Migration.Services.Implementations;
|
namespace Bit.Core.Billing.Providers.Migration.Services.Implementations;
|
||||||
|
|
||||||
public class ProviderMigrator(
|
public class ProviderMigrator(
|
||||||
IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository,
|
IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository,
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.Billing.Models;
|
namespace Bit.Core.Billing.Providers.Models;
|
||||||
|
|
||||||
public record AddableOrganization(
|
public record AddableOrganization(
|
||||||
Guid Id,
|
Guid Id,
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Services.Contracts;
|
namespace Bit.Core.Billing.Providers.Models;
|
||||||
|
|
||||||
public record ChangeProviderPlanCommand(
|
public record ChangeProviderPlanCommand(
|
||||||
Provider Provider,
|
Provider Provider,
|
@ -1,11 +1,12 @@
|
|||||||
using Bit.Core.Models.StaticStore;
|
using Bit.Core.Models.StaticStore;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Models;
|
namespace Bit.Core.Billing.Providers.Models;
|
||||||
|
|
||||||
public record ConfiguredProviderPlan(
|
public record ConfiguredProviderPlan(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid ProviderId,
|
Guid ProviderId,
|
||||||
Plan Plan,
|
Plan Plan,
|
||||||
|
decimal Price,
|
||||||
int SeatMinimum,
|
int SeatMinimum,
|
||||||
int PurchasedSeats,
|
int PurchasedSeats,
|
||||||
int AssignedSeats);
|
int AssignedSeats);
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Services.Contracts;
|
namespace Bit.Core.Billing.Providers.Models;
|
||||||
|
|
||||||
/// <param name="Provider">The provider to update the seat minimums for.</param>
|
/// <param name="Provider">The provider to update the seat minimums for.</param>
|
||||||
/// <param name="Configuration">The new seat minimums for the provider.</param>
|
/// <param name="Configuration">The new seat minimums for the provider.</param>
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Repositories;
|
namespace Bit.Core.Billing.Providers.Repositories;
|
||||||
|
|
||||||
public interface IClientOrganizationMigrationRecordRepository : IRepository<ClientOrganizationMigrationRecord, Guid>
|
public interface IClientOrganizationMigrationRecordRepository : IRepository<ClientOrganizationMigrationRecord, Guid>
|
||||||
{
|
{
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Repositories;
|
namespace Bit.Core.Billing.Providers.Repositories;
|
||||||
|
|
||||||
public interface IProviderInvoiceItemRepository : IRepository<ProviderInvoiceItem, Guid>
|
public interface IProviderInvoiceItemRepository : IRepository<ProviderInvoiceItem, Guid>
|
||||||
{
|
{
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Repositories;
|
namespace Bit.Core.Billing.Providers.Repositories;
|
||||||
|
|
||||||
public interface IProviderPlanRepository : IRepository<ProviderPlan, Guid>
|
public interface IProviderPlanRepository : IRepository<ProviderPlan, Guid>
|
||||||
{
|
{
|
@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using OneOf;
|
using OneOf;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Services;
|
namespace Bit.Core.Billing.Providers.Services;
|
||||||
|
|
||||||
public interface IBusinessUnitConverter
|
public interface IBusinessUnitConverter
|
||||||
{
|
{
|
@ -1,14 +1,14 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Billing.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Providers.Entities;
|
||||||
|
using Bit.Core.Billing.Providers.Models;
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Services;
|
namespace Bit.Core.Billing.Providers.Services;
|
||||||
|
|
||||||
public interface IProviderBillingService
|
public interface IProviderBillingService
|
||||||
{
|
{
|
@ -1,11 +1,11 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Billing.Tax.Services;
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -35,16 +35,15 @@ public class OrganizationBillingService(
|
|||||||
ISetupIntentCache setupIntentCache,
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
ITaxService taxService,
|
ITaxService taxService) : IOrganizationBillingService
|
||||||
IAutomaticTaxFactory automaticTaxFactory) : IOrganizationBillingService
|
|
||||||
{
|
{
|
||||||
public async Task Finalize(OrganizationSale sale)
|
public async Task Finalize(OrganizationSale sale)
|
||||||
{
|
{
|
||||||
var (organization, customerSetup, subscriptionSetup) = sale;
|
var (organization, customerSetup, subscriptionSetup) = sale;
|
||||||
|
|
||||||
var customer = string.IsNullOrEmpty(organization.GatewayCustomerId) && customerSetup != null
|
var customer = string.IsNullOrEmpty(organization.GatewayCustomerId) && customerSetup != null
|
||||||
? await CreateCustomerAsync(organization, customerSetup)
|
? await CreateCustomerAsync(organization, customerSetup, subscriptionSetup.PlanType)
|
||||||
: await subscriberService.GetCustomerOrThrow(organization, new CustomerGetOptions { Expand = ["tax", "tax_ids"] });
|
: await GetCustomerWhileEnsuringCorrectTaxExemptionAsync(organization, subscriptionSetup);
|
||||||
|
|
||||||
var subscription = await CreateSubscriptionAsync(organization.Id, customer, subscriptionSetup);
|
var subscription = await CreateSubscriptionAsync(organization.Id, customer, subscriptionSetup);
|
||||||
|
|
||||||
@ -121,7 +120,8 @@ public class OrganizationBillingService(
|
|||||||
subscription.CurrentPeriodEnd);
|
subscription.CurrentPeriodEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdatePaymentMethod(
|
public async Task
|
||||||
|
UpdatePaymentMethod(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
TokenizedPaymentSource tokenizedPaymentSource,
|
TokenizedPaymentSource tokenizedPaymentSource,
|
||||||
TaxInformation taxInformation)
|
TaxInformation taxInformation)
|
||||||
@ -151,8 +151,11 @@ public class OrganizationBillingService(
|
|||||||
|
|
||||||
private async Task<Customer> CreateCustomerAsync(
|
private async Task<Customer> CreateCustomerAsync(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
CustomerSetup customerSetup)
|
CustomerSetup customerSetup,
|
||||||
|
PlanType? updatedPlanType = null)
|
||||||
{
|
{
|
||||||
|
var planType = updatedPlanType ?? organization.PlanType;
|
||||||
|
|
||||||
var displayName = organization.DisplayName();
|
var displayName = organization.DisplayName();
|
||||||
|
|
||||||
var customerCreateOptions = new CustomerCreateOptions
|
var customerCreateOptions = new CustomerCreateOptions
|
||||||
@ -212,13 +215,24 @@ public class OrganizationBillingService(
|
|||||||
City = customerSetup.TaxInformation.City,
|
City = customerSetup.TaxInformation.City,
|
||||||
PostalCode = customerSetup.TaxInformation.PostalCode,
|
PostalCode = customerSetup.TaxInformation.PostalCode,
|
||||||
State = customerSetup.TaxInformation.State,
|
State = customerSetup.TaxInformation.State,
|
||||||
Country = customerSetup.TaxInformation.Country,
|
Country = customerSetup.TaxInformation.Country
|
||||||
};
|
};
|
||||||
|
|
||||||
customerCreateOptions.Tax = new CustomerTaxOptions
|
customerCreateOptions.Tax = new CustomerTaxOptions
|
||||||
{
|
{
|
||||||
ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately
|
ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var setNonUSBusinessUseToReverseCharge =
|
||||||
|
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge &&
|
||||||
|
planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families &&
|
||||||
|
customerSetup.TaxInformation.Country != "US")
|
||||||
|
{
|
||||||
|
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(customerSetup.TaxInformation.TaxId))
|
if (!string.IsNullOrEmpty(customerSetup.TaxInformation.TaxId))
|
||||||
{
|
{
|
||||||
var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country,
|
var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country,
|
||||||
@ -399,21 +413,68 @@ public class OrganizationBillingService(
|
|||||||
TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays
|
TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays
|
||||||
};
|
};
|
||||||
|
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
var setNonUSBusinessUseToReverseCharge =
|
||||||
|
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge)
|
||||||
{
|
{
|
||||||
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriptionSetup.PlanType);
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters);
|
|
||||||
automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
|
|
||||||
}
|
}
|
||||||
else
|
else if (customer.HasRecognizedTaxLocation())
|
||||||
{
|
{
|
||||||
subscriptionCreateOptions.AutomaticTax ??= new SubscriptionAutomaticTaxOptions();
|
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||||
subscriptionCreateOptions.AutomaticTax.Enabled = customer.HasBillingLocation();
|
{
|
||||||
|
Enabled =
|
||||||
|
subscriptionSetup.PlanType.GetProductTier() == ProductTierType.Families ||
|
||||||
|
customer.Address.Country == "US" ||
|
||||||
|
customer.TaxIds.Any()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Customer> GetCustomerWhileEnsuringCorrectTaxExemptionAsync(
|
||||||
|
Organization organization,
|
||||||
|
SubscriptionSetup subscriptionSetup)
|
||||||
|
{
|
||||||
|
var customer = await subscriberService.GetCustomerOrThrow(organization,
|
||||||
|
new CustomerGetOptions { Expand = ["tax", "tax_ids"] });
|
||||||
|
|
||||||
|
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (!setNonUSBusinessUseToReverseCharge || subscriptionSetup.PlanType.GetProductTier() is
|
||||||
|
not (ProductTierType.Teams or
|
||||||
|
ProductTierType.TeamsStarter or
|
||||||
|
ProductTierType.Enterprise))
|
||||||
|
{
|
||||||
|
return customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> expansions = ["tax", "tax_ids"];
|
||||||
|
|
||||||
|
customer = customer switch
|
||||||
|
{
|
||||||
|
{ Address.Country: not "US", TaxExempt: not StripeConstants.TaxExempt.Reverse } => await
|
||||||
|
stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||||
|
new CustomerUpdateOptions
|
||||||
|
{
|
||||||
|
Expand = expansions,
|
||||||
|
TaxExempt = StripeConstants.TaxExempt.Reverse
|
||||||
|
}),
|
||||||
|
{ Address.Country: "US", TaxExempt: StripeConstants.TaxExempt.Reverse } => await
|
||||||
|
stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||||
|
new CustomerUpdateOptions
|
||||||
|
{
|
||||||
|
Expand = expansions,
|
||||||
|
TaxExempt = StripeConstants.TaxExempt.None
|
||||||
|
}),
|
||||||
|
_ => customer
|
||||||
|
};
|
||||||
|
|
||||||
|
return customer;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> IsEligibleForSelfHostAsync(
|
private async Task<bool> IsEligibleForSelfHostAsync(
|
||||||
Organization organization)
|
Organization organization)
|
||||||
{
|
{
|
||||||
|
@ -3,8 +3,6 @@ using Bit.Core.Billing.Constants;
|
|||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Billing.Tax.Services;
|
|
||||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -12,7 +10,6 @@ using Bit.Core.Repositories;
|
|||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Braintree;
|
using Braintree;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using Customer = Stripe.Customer;
|
using Customer = Stripe.Customer;
|
||||||
@ -24,20 +21,18 @@ using static Utilities;
|
|||||||
|
|
||||||
public class PremiumUserBillingService(
|
public class PremiumUserBillingService(
|
||||||
IBraintreeGateway braintreeGateway,
|
IBraintreeGateway braintreeGateway,
|
||||||
IFeatureService featureService,
|
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ILogger<PremiumUserBillingService> logger,
|
ILogger<PremiumUserBillingService> logger,
|
||||||
ISetupIntentCache setupIntentCache,
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository) : IPremiumUserBillingService
|
||||||
[FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy automaticTaxStrategy) : IPremiumUserBillingService
|
|
||||||
{
|
{
|
||||||
public async Task Credit(User user, decimal amount)
|
public async Task Credit(User user, decimal amount)
|
||||||
{
|
{
|
||||||
var customer = await subscriberService.GetCustomer(user);
|
var customer = await subscriberService.GetCustomer(user);
|
||||||
|
|
||||||
// Negative credit represents a balance and all Stripe denomination is in cents.
|
// Negative credit represents a balance, and all Stripe denomination is in cents.
|
||||||
var credit = (long)(amount * -100);
|
var credit = (long)(amount * -100);
|
||||||
|
|
||||||
if (customer == null)
|
if (customer == null)
|
||||||
@ -184,7 +179,7 @@ public class PremiumUserBillingService(
|
|||||||
City = customerSetup.TaxInformation.City,
|
City = customerSetup.TaxInformation.City,
|
||||||
PostalCode = customerSetup.TaxInformation.PostalCode,
|
PostalCode = customerSetup.TaxInformation.PostalCode,
|
||||||
State = customerSetup.TaxInformation.State,
|
State = customerSetup.TaxInformation.State,
|
||||||
Country = customerSetup.TaxInformation.Country,
|
Country = customerSetup.TaxInformation.Country
|
||||||
},
|
},
|
||||||
Description = user.Name,
|
Description = user.Name,
|
||||||
Email = user.Email,
|
Email = user.Email,
|
||||||
@ -324,6 +319,10 @@ public class PremiumUserBillingService(
|
|||||||
|
|
||||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||||
{
|
{
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||||
|
{
|
||||||
|
Enabled = true
|
||||||
|
},
|
||||||
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
|
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
|
||||||
Customer = customer.Id,
|
Customer = customer.Id,
|
||||||
Items = subscriptionItemOptionsList,
|
Items = subscriptionItemOptionsList,
|
||||||
@ -337,18 +336,6 @@ public class PremiumUserBillingService(
|
|||||||
OffSession = true
|
OffSession = true
|
||||||
};
|
};
|
||||||
|
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
|
||||||
{
|
|
||||||
automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
|
||||||
{
|
|
||||||
Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
|
|
||||||
if (usingPayPal)
|
if (usingPayPal)
|
||||||
@ -380,7 +367,7 @@ public class PremiumUserBillingService(
|
|||||||
City = taxInformation.City,
|
City = taxInformation.City,
|
||||||
PostalCode = taxInformation.PostalCode,
|
PostalCode = taxInformation.PostalCode,
|
||||||
State = taxInformation.State,
|
State = taxInformation.State,
|
||||||
Country = taxInformation.Country,
|
Country = taxInformation.Country
|
||||||
},
|
},
|
||||||
Expand = ["tax"],
|
Expand = ["tax"],
|
||||||
Tax = new CustomerTaxOptions
|
Tax = new CustomerTaxOptions
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Billing.Tax.Services;
|
using Bit.Core.Billing.Tax.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@ -28,8 +31,7 @@ public class SubscriberService(
|
|||||||
ILogger<SubscriberService> logger,
|
ILogger<SubscriberService> logger,
|
||||||
ISetupIntentCache setupIntentCache,
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ITaxService taxService,
|
ITaxService taxService) : ISubscriberService
|
||||||
IAutomaticTaxFactory automaticTaxFactory) : ISubscriberService
|
|
||||||
{
|
{
|
||||||
public async Task CancelSubscription(
|
public async Task CancelSubscription(
|
||||||
ISubscriber subscriber,
|
ISubscriber subscriber,
|
||||||
@ -128,7 +130,7 @@ public class SubscriberService(
|
|||||||
[subscriber.BraintreeCloudRegionField()] = globalSettings.BaseServiceUri.CloudRegion
|
[subscriber.BraintreeCloudRegionField()] = globalSettings.BaseServiceUri.CloudRegion
|
||||||
},
|
},
|
||||||
Email = subscriber.BillingEmailAddress(),
|
Email = subscriber.BillingEmailAddress(),
|
||||||
PaymentMethodNonce = paymentMethodNonce,
|
PaymentMethodNonce = paymentMethodNonce
|
||||||
});
|
});
|
||||||
|
|
||||||
if (customerResult.IsSuccess())
|
if (customerResult.IsSuccess())
|
||||||
@ -482,7 +484,7 @@ public class SubscriberService(
|
|||||||
|
|
||||||
var matchingSetupIntent = setupIntentsForUpdatedPaymentMethod.First();
|
var matchingSetupIntent = setupIntentsForUpdatedPaymentMethod.First();
|
||||||
|
|
||||||
// Find the customer's existing setup intents that should be cancelled.
|
// Find the customer's existing setup intents that should be canceled.
|
||||||
var existingSetupIntentsForCustomer = (await getExistingSetupIntentsForCustomer)
|
var existingSetupIntentsForCustomer = (await getExistingSetupIntentsForCustomer)
|
||||||
.Where(si =>
|
.Where(si =>
|
||||||
si.Status is "requires_payment_method" or "requires_confirmation" or "requires_action");
|
si.Status is "requires_payment_method" or "requires_confirmation" or "requires_action");
|
||||||
@ -519,7 +521,7 @@ public class SubscriberService(
|
|||||||
await stripeAdapter.PaymentMethodAttachAsync(token,
|
await stripeAdapter.PaymentMethodAttachAsync(token,
|
||||||
new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId });
|
new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId });
|
||||||
|
|
||||||
// Find the customer's existing setup intents that should be cancelled.
|
// Find the customer's existing setup intents that should be canceled.
|
||||||
var existingSetupIntentsForCustomer = (await getExistingSetupIntentsForCustomer)
|
var existingSetupIntentsForCustomer = (await getExistingSetupIntentsForCustomer)
|
||||||
.Where(si =>
|
.Where(si =>
|
||||||
si.Status is "requires_payment_method" or "requires_confirmation" or "requires_action");
|
si.Status is "requires_payment_method" or "requires_confirmation" or "requires_action");
|
||||||
@ -637,7 +639,8 @@ public class SubscriberService(
|
|||||||
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
||||||
taxInformation.Country,
|
taxInformation.Country,
|
||||||
taxInformation.TaxId);
|
taxInformation.TaxId);
|
||||||
throw new Exceptions.BadRequestException("billingTaxIdTypeInferenceError");
|
|
||||||
|
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -654,53 +657,84 @@ public class SubscriberService(
|
|||||||
logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.",
|
logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.",
|
||||||
taxInformation.TaxId,
|
taxInformation.TaxId,
|
||||||
taxInformation.Country);
|
taxInformation.Country);
|
||||||
throw new Exceptions.BadRequestException("billingInvalidTaxIdError");
|
|
||||||
|
throw new BadRequestException("billingInvalidTaxIdError");
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.LogError(e,
|
logger.LogError(e,
|
||||||
"Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.",
|
"Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.",
|
||||||
taxInformation.TaxId,
|
taxInformation.TaxId,
|
||||||
taxInformation.Country,
|
taxInformation.Country,
|
||||||
customer.Id);
|
customer.Id);
|
||||||
throw new Exceptions.BadRequestException("billingTaxIdCreationError");
|
|
||||||
|
throw new BadRequestException("billingTaxIdCreationError");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
var subscription =
|
||||||
|
customer.Subscriptions.First(subscription => subscription.Id == subscriber.GatewaySubscriptionId);
|
||||||
|
|
||||||
|
var isBusinessUseSubscriber = subscriber switch
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))
|
Organization organization => organization.PlanType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families,
|
||||||
|
Provider => true,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
|
||||||
|
var setNonUSBusinessUseToReverseCharge =
|
||||||
|
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge && isBusinessUseSubscriber)
|
||||||
|
{
|
||||||
|
switch (customer)
|
||||||
{
|
{
|
||||||
var subscriptionGetOptions = new SubscriptionGetOptions
|
case
|
||||||
{
|
{
|
||||||
Expand = ["customer.tax", "customer.tax_ids"]
|
Address.Country: not "US",
|
||||||
};
|
TaxExempt: not StripeConstants.TaxExempt.Reverse
|
||||||
var subscription = await stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions);
|
}:
|
||||||
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, subscription.Items.Select(x => x.Price.Id));
|
await stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||||
var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters);
|
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||||
var automaticTaxOptions = automaticTaxStrategy.GetUpdateOptions(subscription);
|
break;
|
||||||
if (automaticTaxOptions?.AutomaticTax?.Enabled != null)
|
case
|
||||||
{
|
{
|
||||||
await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, automaticTaxOptions);
|
Address.Country: "US",
|
||||||
}
|
TaxExempt: StripeConstants.TaxExempt.Reverse
|
||||||
|
}:
|
||||||
|
await stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||||
|
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.None });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
if (!subscription.AutomaticTax.Enabled)
|
||||||
{
|
|
||||||
if (SubscriberIsEligibleForAutomaticTax(subscriber, customer))
|
|
||||||
{
|
{
|
||||||
await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId,
|
await stripeAdapter.SubscriptionUpdateAsync(subscription.Id,
|
||||||
new SubscriptionUpdateOptions
|
new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var automaticTaxShouldBeEnabled = subscriber switch
|
||||||
|
{
|
||||||
|
User => true,
|
||||||
|
Organization organization => organization.PlanType.GetProductTier() == ProductTierType.Families ||
|
||||||
|
customer.Address.Country == "US" || (customer.TaxIds?.Any() ?? false),
|
||||||
|
Provider => customer.Address.Country == "US" || (customer.TaxIds?.Any() ?? false),
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
|
||||||
return;
|
if (automaticTaxShouldBeEnabled && !subscription.AutomaticTax.Enabled)
|
||||||
|
{
|
||||||
bool SubscriberIsEligibleForAutomaticTax(ISubscriber localSubscriber, Customer localCustomer)
|
await stripeAdapter.SubscriptionUpdateAsync(subscription.Id,
|
||||||
=> !string.IsNullOrEmpty(localSubscriber.GatewaySubscriptionId) &&
|
new SubscriptionUpdateOptions
|
||||||
(localCustomer.Subscriptions?.Any(sub => sub.Id == localSubscriber.GatewaySubscriptionId && !sub.AutomaticTax.Enabled) ?? false) &&
|
{
|
||||||
localCustomer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Services.Contracts;
|
namespace Bit.Core.Billing.Tax.Models;
|
||||||
|
|
||||||
public class AutomaticTaxFactoryParameters
|
public class AutomaticTaxFactoryParameters
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Tax.Services;
|
namespace Bit.Core.Billing.Tax.Services;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ public class BusinessUseAutomaticTaxStrategy(IFeatureService featureService) : I
|
|||||||
|
|
||||||
private bool ShouldBeEnabled(Customer customer)
|
private bool ShouldBeEnabled(Customer customer)
|
||||||
{
|
{
|
||||||
if (!customer.HasTaxLocationVerified())
|
if (!customer.HasRecognizedTaxLocation())
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,6 @@ public class PersonalUseAutomaticTaxStrategy(IFeatureService featureService) : I
|
|||||||
|
|
||||||
private static bool ShouldBeEnabled(Customer customer)
|
private static bool ShouldBeEnabled(Customer customer)
|
||||||
{
|
{
|
||||||
return customer.HasTaxLocationVerified();
|
return customer.HasRecognizedTaxLocation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,13 +143,14 @@ public static class FeatureFlagKeys
|
|||||||
public const string UsePricingService = "use-pricing-service";
|
public const string UsePricingService = "use-pricing-service";
|
||||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||||
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||||
public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements";
|
|
||||||
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
||||||
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
|
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
|
||||||
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
|
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
|
||||||
public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup";
|
public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup";
|
||||||
public const string UseOrganizationWarningsService = "use-organization-warnings-service";
|
public const string UseOrganizationWarningsService = "use-organization-warnings-service";
|
||||||
public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0";
|
public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0";
|
||||||
|
public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge";
|
||||||
|
public const string PM21383_GetProviderPriceFromStripe = "pm-21383-get-provider-price-from-stripe";
|
||||||
|
|
||||||
/* Data Insights and Reporting Team */
|
/* Data Insights and Reporting Team */
|
||||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||||
|
@ -69,8 +69,11 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddBaseOrganizationSubscriptionCommandsQueries();
|
services.AddBaseOrganizationSubscriptionCommandsQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) =>
|
private static void AddOrganizationSignUpCommands(this IServiceCollection services)
|
||||||
|
{
|
||||||
services.AddScoped<ICloudOrganizationSignUpCommand, CloudOrganizationSignUpCommand>();
|
services.AddScoped<ICloudOrganizationSignUpCommand, CloudOrganizationSignUpCommand>();
|
||||||
|
services.AddScoped<IProviderClientOrganizationSignUpCommand, ProviderClientOrganizationSignUpCommand>();
|
||||||
|
}
|
||||||
|
|
||||||
private static void AddOrganizationDeleteCommands(this IServiceCollection services)
|
private static void AddOrganizationDeleteCommands(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
@ -4,7 +4,6 @@ using Bit.Core.Billing.Models;
|
|||||||
using Bit.Core.Billing.Tax.Requests;
|
using Bit.Core.Billing.Tax.Requests;
|
||||||
using Bit.Core.Billing.Tax.Responses;
|
using Bit.Core.Billing.Tax.Responses;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.StaticStore;
|
using Bit.Core.Models.StaticStore;
|
||||||
|
|
||||||
@ -30,8 +29,6 @@ public interface IPaymentService
|
|||||||
Task<string> AdjustServiceAccountsAsync(Organization organization, Plan plan, int additionalServiceAccounts);
|
Task<string> AdjustServiceAccountsAsync(Organization organization, Plan plan, int additionalServiceAccounts);
|
||||||
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);
|
Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);
|
||||||
Task ReinstateSubscriptionAsync(ISubscriber subscriber);
|
Task ReinstateSubscriptionAsync(ISubscriber subscriber);
|
||||||
Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType,
|
|
||||||
string paymentToken, TaxInfo taxInfo = null);
|
|
||||||
Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount);
|
Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount);
|
||||||
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
|
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
|
||||||
Task<BillingHistoryInfo> GetBillingHistoryAsync(ISubscriber subscriber);
|
Task<BillingHistoryInfo> GetBillingHistoryAsync(ISubscriber subscriber);
|
||||||
|
@ -57,4 +57,5 @@ public interface IStripeAdapter
|
|||||||
Task<SetupIntent> SetupIntentGet(string id, SetupIntentGetOptions options = null);
|
Task<SetupIntent> SetupIntentGet(string id, SetupIntentGetOptions options = null);
|
||||||
Task SetupIntentVerifyMicroDeposit(string id, SetupIntentVerifyMicrodepositsOptions options);
|
Task SetupIntentVerifyMicroDeposit(string id, SetupIntentVerifyMicrodepositsOptions options);
|
||||||
Task<List<Stripe.TestHelpers.TestClock>> TestClockListAsync();
|
Task<List<Stripe.TestHelpers.TestClock>> TestClockListAsync();
|
||||||
|
Task<Price> PriceGetAsync(string id, PriceGetOptions options = null);
|
||||||
}
|
}
|
||||||
|
@ -283,4 +283,7 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<Price> PriceGetAsync(string id, PriceGetOptions options = null)
|
||||||
|
=> _priceService.GetAsync(id, options);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Business;
|
using Bit.Core.Billing.Models.Business;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
|
||||||
using Bit.Core.Billing.Services.Contracts;
|
|
||||||
using Bit.Core.Billing.Tax.Models;
|
using Bit.Core.Billing.Tax.Models;
|
||||||
using Bit.Core.Billing.Tax.Requests;
|
using Bit.Core.Billing.Tax.Requests;
|
||||||
using Bit.Core.Billing.Tax.Responses;
|
using Bit.Core.Billing.Tax.Responses;
|
||||||
@ -38,7 +38,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly ITaxService _taxService;
|
private readonly ITaxService _taxService;
|
||||||
private readonly ISubscriberService _subscriberService;
|
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly IAutomaticTaxFactory _automaticTaxFactory;
|
private readonly IAutomaticTaxFactory _automaticTaxFactory;
|
||||||
private readonly IAutomaticTaxStrategy _personalUseTaxStrategy;
|
private readonly IAutomaticTaxStrategy _personalUseTaxStrategy;
|
||||||
@ -51,7 +50,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
ITaxService taxService,
|
ITaxService taxService,
|
||||||
ISubscriberService subscriberService,
|
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
IAutomaticTaxFactory automaticTaxFactory,
|
IAutomaticTaxFactory automaticTaxFactory,
|
||||||
[FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy personalUseTaxStrategy)
|
[FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy personalUseTaxStrategy)
|
||||||
@ -63,7 +61,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_taxService = taxService;
|
_taxService = taxService;
|
||||||
_subscriberService = subscriberService;
|
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
_automaticTaxFactory = automaticTaxFactory;
|
_automaticTaxFactory = automaticTaxFactory;
|
||||||
_personalUseTaxStrategy = personalUseTaxStrategy;
|
_personalUseTaxStrategy = personalUseTaxStrategy;
|
||||||
@ -136,15 +133,68 @@ public class StripePaymentService : IPaymentService
|
|||||||
|
|
||||||
if (subscriptionUpdate is CompleteSubscriptionUpdate)
|
if (subscriptionUpdate is CompleteSubscriptionUpdate)
|
||||||
{
|
{
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
var setNonUSBusinessUseToReverseCharge =
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||||
|
|
||||||
|
if (setNonUSBusinessUseToReverseCharge)
|
||||||
{
|
{
|
||||||
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, updatedItemOptions.Select(x => x.Plan ?? x.Price));
|
if (sub.Customer is
|
||||||
var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxParameters);
|
{
|
||||||
automaticTaxStrategy.SetUpdateOptions(subUpdateOptions, sub);
|
Address.Country: not "US",
|
||||||
|
TaxExempt: not StripeConstants.TaxExempt.Reverse
|
||||||
|
})
|
||||||
|
{
|
||||||
|
await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId,
|
||||||
|
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||||
|
}
|
||||||
|
|
||||||
|
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
}
|
}
|
||||||
else
|
else if (sub.Customer.HasRecognizedTaxLocation())
|
||||||
{
|
{
|
||||||
subUpdateOptions.EnableAutomaticTax(sub.Customer, sub);
|
switch (subscriber)
|
||||||
|
{
|
||||||
|
case User:
|
||||||
|
{
|
||||||
|
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Organization:
|
||||||
|
{
|
||||||
|
if (sub.Customer.Address.Country == "US")
|
||||||
|
{
|
||||||
|
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var familyPriceIds = (await Task.WhenAll(
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
|
||||||
|
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
|
||||||
|
.Select(plan => plan.PasswordManager.StripePlanId);
|
||||||
|
|
||||||
|
var updateIsForPersonalUse = updatedItemOptions
|
||||||
|
.Select(option => option.Price)
|
||||||
|
.Intersect(familyPriceIds)
|
||||||
|
.Any();
|
||||||
|
|
||||||
|
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||||
|
{
|
||||||
|
Enabled = updateIsForPersonalUse || sub.Customer.TaxIds.Any()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Provider:
|
||||||
|
{
|
||||||
|
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||||
|
{
|
||||||
|
Enabled = sub.Customer.Address.Country == "US" ||
|
||||||
|
sub.Customer.TaxIds.Any()
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,7 +252,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
else if (!invoice.Paid)
|
else if (!invoice.Paid)
|
||||||
{
|
{
|
||||||
// Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h
|
// Pay invoice with no charge to the customer this completes the invoice immediately without waiting the scheduled 1h
|
||||||
invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId);
|
invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId);
|
||||||
paymentIntentClientSecret = null;
|
paymentIntentClientSecret = null;
|
||||||
}
|
}
|
||||||
@ -585,309 +635,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType,
|
|
||||||
string paymentToken, TaxInfo taxInfo = null)
|
|
||||||
{
|
|
||||||
if (subscriber == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(subscriber));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscriber.Gateway.HasValue && subscriber.Gateway.Value != GatewayType.Stripe)
|
|
||||||
{
|
|
||||||
throw new GatewayException("Switching from one payment type to another is not supported. " +
|
|
||||||
"Contact us for assistance.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var createdCustomer = false;
|
|
||||||
Braintree.Customer braintreeCustomer = null;
|
|
||||||
string stipeCustomerSourceToken = null;
|
|
||||||
string stipeCustomerPaymentMethodId = null;
|
|
||||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
|
||||||
};
|
|
||||||
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount;
|
|
||||||
|
|
||||||
Customer customer = null;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
|
||||||
{
|
|
||||||
var options = new CustomerGetOptions { Expand = ["sources", "tax", "subscriptions"] };
|
|
||||||
customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, options);
|
|
||||||
if (customer.Metadata?.Any() ?? false)
|
|
||||||
{
|
|
||||||
stripeCustomerMetadata = customer.Metadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hadBtCustomer = stripeCustomerMetadata.ContainsKey("btCustomerId");
|
|
||||||
if (stripePaymentMethod)
|
|
||||||
{
|
|
||||||
if (paymentToken.StartsWith("pm_"))
|
|
||||||
{
|
|
||||||
stipeCustomerPaymentMethodId = paymentToken;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
stipeCustomerSourceToken = paymentToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (paymentMethodType == PaymentMethodType.PayPal)
|
|
||||||
{
|
|
||||||
if (hadBtCustomer)
|
|
||||||
{
|
|
||||||
var pmResult = await _btGateway.PaymentMethod.CreateAsync(new Braintree.PaymentMethodRequest
|
|
||||||
{
|
|
||||||
CustomerId = stripeCustomerMetadata["btCustomerId"],
|
|
||||||
PaymentMethodNonce = paymentToken
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pmResult.IsSuccess())
|
|
||||||
{
|
|
||||||
var customerResult = await _btGateway.Customer.UpdateAsync(
|
|
||||||
stripeCustomerMetadata["btCustomerId"], new Braintree.CustomerRequest
|
|
||||||
{
|
|
||||||
DefaultPaymentMethodToken = pmResult.Target.Token
|
|
||||||
});
|
|
||||||
|
|
||||||
if (customerResult.IsSuccess() && customerResult.Target.PaymentMethods.Length > 0)
|
|
||||||
{
|
|
||||||
braintreeCustomer = customerResult.Target;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _btGateway.PaymentMethod.DeleteAsync(pmResult.Target.Token);
|
|
||||||
hadBtCustomer = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
hadBtCustomer = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hadBtCustomer)
|
|
||||||
{
|
|
||||||
var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest
|
|
||||||
{
|
|
||||||
PaymentMethodNonce = paymentToken,
|
|
||||||
Email = subscriber.BillingEmailAddress(),
|
|
||||||
Id = subscriber.BraintreeCustomerIdPrefix() + subscriber.Id.ToString("N").ToLower() +
|
|
||||||
Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false),
|
|
||||||
CustomFields = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
[subscriber.BraintreeIdField()] = subscriber.Id.ToString(),
|
|
||||||
[subscriber.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
|
|
||||||
{
|
|
||||||
throw new GatewayException("Failed to create PayPal customer record.");
|
|
||||||
}
|
|
||||||
|
|
||||||
braintreeCustomer = customerResult.Target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new GatewayException("Payment method is not supported at this time.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stripeCustomerMetadata.ContainsKey("btCustomerId"))
|
|
||||||
{
|
|
||||||
if (braintreeCustomer?.Id != stripeCustomerMetadata["btCustomerId"])
|
|
||||||
{
|
|
||||||
stripeCustomerMetadata["btCustomerId_old"] = stripeCustomerMetadata["btCustomerId"];
|
|
||||||
}
|
|
||||||
|
|
||||||
stripeCustomerMetadata["btCustomerId"] = braintreeCustomer?.Id;
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrWhiteSpace(braintreeCustomer?.Id))
|
|
||||||
{
|
|
||||||
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber))
|
|
||||||
{
|
|
||||||
taxInfo.TaxIdType = taxInfo.TaxIdType ??
|
|
||||||
_taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customer == null)
|
|
||||||
{
|
|
||||||
customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions
|
|
||||||
{
|
|
||||||
Description = subscriber.BillingName(),
|
|
||||||
Email = subscriber.BillingEmailAddress(),
|
|
||||||
Metadata = stripeCustomerMetadata,
|
|
||||||
Source = stipeCustomerSourceToken,
|
|
||||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
|
||||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
|
||||||
{
|
|
||||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
|
||||||
CustomFields =
|
|
||||||
[
|
|
||||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
|
||||||
{
|
|
||||||
Name = subscriber.SubscriberType(),
|
|
||||||
Value = subscriber.GetFormattedInvoiceName()
|
|
||||||
}
|
|
||||||
|
|
||||||
]
|
|
||||||
},
|
|
||||||
Address = taxInfo == null ? null : new AddressOptions
|
|
||||||
{
|
|
||||||
Country = taxInfo.BillingAddressCountry,
|
|
||||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
|
||||||
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
|
|
||||||
Line2 = taxInfo.BillingAddressLine2,
|
|
||||||
City = taxInfo.BillingAddressCity,
|
|
||||||
State = taxInfo.BillingAddressState
|
|
||||||
},
|
|
||||||
TaxIdData = string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
new CustomerTaxIdDataOptions
|
|
||||||
{
|
|
||||||
Type = taxInfo.TaxIdType,
|
|
||||||
Value = taxInfo.TaxIdNumber
|
|
||||||
}
|
|
||||||
],
|
|
||||||
Expand = ["sources", "tax", "subscriptions"],
|
|
||||||
});
|
|
||||||
|
|
||||||
subscriber.Gateway = GatewayType.Stripe;
|
|
||||||
subscriber.GatewayCustomerId = customer.Id;
|
|
||||||
createdCustomer = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!createdCustomer)
|
|
||||||
{
|
|
||||||
string defaultSourceId = null;
|
|
||||||
string defaultPaymentMethodId = null;
|
|
||||||
if (stripePaymentMethod)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(stipeCustomerSourceToken) && paymentToken.StartsWith("btok_"))
|
|
||||||
{
|
|
||||||
var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new BankAccountCreateOptions
|
|
||||||
{
|
|
||||||
Source = paymentToken
|
|
||||||
});
|
|
||||||
defaultSourceId = bankAccount.Id;
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId))
|
|
||||||
{
|
|
||||||
await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId,
|
|
||||||
new PaymentMethodAttachOptions { Customer = customer.Id });
|
|
||||||
defaultPaymentMethodId = stipeCustomerPaymentMethodId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customer.Sources != null)
|
|
||||||
{
|
|
||||||
foreach (var source in customer.Sources.Where(s => s.Id != defaultSourceId))
|
|
||||||
{
|
|
||||||
if (source is BankAccount)
|
|
||||||
{
|
|
||||||
await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id);
|
|
||||||
}
|
|
||||||
else if (source is Card)
|
|
||||||
{
|
|
||||||
await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new PaymentMethodListOptions
|
|
||||||
{
|
|
||||||
Customer = customer.Id,
|
|
||||||
Type = "card"
|
|
||||||
});
|
|
||||||
foreach (var cardMethod in cardPaymentMethods.Where(m => m.Id != defaultPaymentMethodId))
|
|
||||||
{
|
|
||||||
await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new PaymentMethodDetachOptions());
|
|
||||||
}
|
|
||||||
|
|
||||||
await _subscriberService.UpdateTaxInformation(subscriber, TaxInformation.From(taxInfo));
|
|
||||||
|
|
||||||
customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
|
||||||
{
|
|
||||||
Metadata = stripeCustomerMetadata,
|
|
||||||
DefaultSource = defaultSourceId,
|
|
||||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
|
||||||
{
|
|
||||||
DefaultPaymentMethod = defaultPaymentMethodId,
|
|
||||||
CustomFields =
|
|
||||||
[
|
|
||||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
|
||||||
{
|
|
||||||
Name = subscriber.SubscriberType(),
|
|
||||||
Value = subscriber.GetFormattedInvoiceName()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
Expand = ["tax", "subscriptions"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))
|
|
||||||
{
|
|
||||||
var subscriptionGetOptions = new SubscriptionGetOptions
|
|
||||||
{
|
|
||||||
Expand = ["customer.tax", "customer.tax_ids"]
|
|
||||||
};
|
|
||||||
var subscription = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions);
|
|
||||||
|
|
||||||
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, subscription.Items.Select(x => x.Price.Id));
|
|
||||||
var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxParameters);
|
|
||||||
var subscriptionUpdateOptions = automaticTaxStrategy.GetUpdateOptions(subscription);
|
|
||||||
|
|
||||||
if (subscriptionUpdateOptions != null)
|
|
||||||
{
|
|
||||||
_ = await _stripeAdapter.SubscriptionUpdateAsync(
|
|
||||||
subscriber.GatewaySubscriptionId,
|
|
||||||
subscriptionUpdateOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) &&
|
|
||||||
customer.Subscriptions.Any(sub =>
|
|
||||||
sub.Id == subscriber.GatewaySubscriptionId &&
|
|
||||||
!sub.AutomaticTax.Enabled) &&
|
|
||||||
customer.HasTaxLocationVerified())
|
|
||||||
{
|
|
||||||
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
|
||||||
{
|
|
||||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
|
|
||||||
DefaultTaxRates = []
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = await _stripeAdapter.SubscriptionUpdateAsync(
|
|
||||||
subscriber.GatewaySubscriptionId,
|
|
||||||
subscriptionUpdateOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
if (braintreeCustomer != null && !hadBtCustomer)
|
|
||||||
{
|
|
||||||
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
|
||||||
}
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createdCustomer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount)
|
public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount)
|
||||||
{
|
{
|
||||||
Customer customer = null;
|
Customer customer = null;
|
||||||
@ -1018,7 +765,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
var address = customer.Address;
|
var address = customer.Address;
|
||||||
var taxId = customer.TaxIds?.FirstOrDefault();
|
var taxId = customer.TaxIds?.FirstOrDefault();
|
||||||
|
|
||||||
// Line1 is required, so if missing we're using the subscriber name
|
// Line1 is required, so if missing we're using the subscriber name,
|
||||||
// see: https://stripe.com/docs/api/customers/create#create_customer-address-line1
|
// see: https://stripe.com/docs/api/customers/create#create_customer-address-line1
|
||||||
if (address != null && string.IsNullOrWhiteSpace(address.Line1))
|
if (address != null && string.IsNullOrWhiteSpace(address.Line1))
|
||||||
{
|
{
|
||||||
|
19
src/Core/Tools/Models/Data/SendAccessResult.cs
Normal file
19
src/Core/Tools/Models/Data/SendAccessResult.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.Models.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This enum represents the possible results when attempting to access a <see cref="Send"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <member>name="Granted">Access is granted for the <see cref="Send"/>.</member>
|
||||||
|
/// <member>name="PasswordRequired">Access is denied, but a password is required to access the <see cref="Send"/>.
|
||||||
|
/// </member>
|
||||||
|
/// <member>name="PasswordInvalid">Access is denied due to an invalid password.</member>
|
||||||
|
/// <member>name="Denied">Access is denied for the <see cref="Send"/>.</member>
|
||||||
|
public enum SendAccessResult
|
||||||
|
{
|
||||||
|
Granted,
|
||||||
|
PasswordRequired,
|
||||||
|
PasswordInvalid,
|
||||||
|
Denied
|
||||||
|
}
|
52
src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs
Normal file
52
src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
using Bit.Core.Tools.Enums;
|
||||||
|
using Bit.Core.Tools.Models.Data;
|
||||||
|
using Bit.Core.Tools.Repositories;
|
||||||
|
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
|
using Bit.Core.Tools.Services;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.SendFeatures.Commands;
|
||||||
|
|
||||||
|
public class AnonymousSendCommand : IAnonymousSendCommand
|
||||||
|
{
|
||||||
|
private readonly ISendRepository _sendRepository;
|
||||||
|
private readonly ISendFileStorageService _sendFileStorageService;
|
||||||
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
|
private readonly ISendAuthorizationService _sendAuthorizationService;
|
||||||
|
|
||||||
|
public AnonymousSendCommand(
|
||||||
|
ISendRepository sendRepository,
|
||||||
|
ISendFileStorageService sendFileStorageService,
|
||||||
|
IPushNotificationService pushNotificationService,
|
||||||
|
ISendAuthorizationService sendAuthorizationService
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_sendRepository = sendRepository;
|
||||||
|
_sendFileStorageService = sendFileStorageService;
|
||||||
|
_pushNotificationService = pushNotificationService;
|
||||||
|
_sendAuthorizationService = sendAuthorizationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response: Send, password required, password invalid
|
||||||
|
public async Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password)
|
||||||
|
{
|
||||||
|
if (send.Type != SendType.File)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Can only get a download URL for a file type of Send");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = _sendAuthorizationService.SendCanBeAccessed(send, password);
|
||||||
|
|
||||||
|
if (!result.Equals(SendAccessResult.Granted))
|
||||||
|
{
|
||||||
|
return (null, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
send.AccessCount++;
|
||||||
|
await _sendRepository.ReplaceAsync(send);
|
||||||
|
await _pushNotificationService.PushSyncSendUpdateAsync(send);
|
||||||
|
return (await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId), result);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
using Bit.Core.Tools.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AnonymousSendCommand interface provides methods for managing anonymous Sends.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAnonymousSendCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Send file download URL for a Send object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send"><see cref="Send" /> used to help get file download url and validate file</param>
|
||||||
|
/// <param name="fileId">FileId get file download url</param>
|
||||||
|
/// <param name="password">A hashed and base64-encoded password. This is compared with the send's password to authorize access.</param>
|
||||||
|
/// <returns>Async Task object with Tuple containing the string of download url and <see cref="SendAccessResult" />
|
||||||
|
/// to determine if the user can access send.
|
||||||
|
/// </returns>
|
||||||
|
Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password);
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
using Bit.Core.Tools.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// NonAnonymousSendCommand interface provides methods for managing non-anonymous Sends.
|
||||||
|
/// </summary>
|
||||||
|
public interface INonAnonymousSendCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Saves a <see cref="Send" /> to the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send"><see cref="Send" /> that will save to database</param>
|
||||||
|
/// <returns>Task completes as <see cref="Send" /> saves to the database</returns>
|
||||||
|
Task SaveSendAsync(Send send);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the <see cref="Send" /> and <see cref="SendFileData" /> to the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send"><see cref="Send" /> that will save to the database</param>
|
||||||
|
/// <param name="data"><see cref="SendFileData" /> that will save to file storage</param>
|
||||||
|
/// <param name="fileLength">Length of file help with saving to file storage</param>
|
||||||
|
/// <returns>Task object for async operations with file upload url</returns>
|
||||||
|
Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Upload a file to an existing <see cref="Send" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream"><see cref="Stream" /> of file to be uploaded. The <see cref="Stream" /> position
|
||||||
|
/// will be set to 0 before uploading the file.</param>
|
||||||
|
/// <param name="send"><see cref="Send" /> used to help with uploading file</param>
|
||||||
|
/// <returns>Task completes after saving <see cref="Stream" /> and <see cref="Send" /> metadata to the file storage</returns>
|
||||||
|
Task UploadFileToExistingSendAsync(Stream stream, Send send);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a <see cref="Send" /> from the database and file storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send"><see cref="Send" /> is used to delete from database and file storage</param>
|
||||||
|
/// <returns>Task completes once <see cref="Send" /> has been deleted from database and file storage.</returns>
|
||||||
|
Task DeleteSendAsync(Send send);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the confirmed file size of a send; when the file size cannot be confirmed, the send is deleted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send">The <see cref="Send" /> this command acts upon</param>
|
||||||
|
/// <returns><see langword="true" /> when the file is confirmed, otherwise <see langword="false" /></returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// When a file size cannot be confirmed, we assume we're working with a rogue client. The send is deleted out of
|
||||||
|
/// an abundance of caution.
|
||||||
|
/// </remarks>
|
||||||
|
Task<bool> ConfirmFileSize(Send send);
|
||||||
|
}
|
180
src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs
Normal file
180
src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
using Bit.Core.Tools.Enums;
|
||||||
|
using Bit.Core.Tools.Models.Business;
|
||||||
|
using Bit.Core.Tools.Models.Data;
|
||||||
|
using Bit.Core.Tools.Repositories;
|
||||||
|
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
|
using Bit.Core.Tools.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.SendFeatures.Commands;
|
||||||
|
|
||||||
|
public class NonAnonymousSendCommand : INonAnonymousSendCommand
|
||||||
|
{
|
||||||
|
private readonly ISendRepository _sendRepository;
|
||||||
|
private readonly ISendFileStorageService _sendFileStorageService;
|
||||||
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
|
private readonly ISendValidationService _sendValidationService;
|
||||||
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly ISendCoreHelperService _sendCoreHelperService;
|
||||||
|
|
||||||
|
public NonAnonymousSendCommand(ISendRepository sendRepository,
|
||||||
|
ISendFileStorageService sendFileStorageService,
|
||||||
|
IPushNotificationService pushNotificationService,
|
||||||
|
ISendAuthorizationService sendAuthorizationService,
|
||||||
|
ISendValidationService sendValidationService,
|
||||||
|
IReferenceEventService referenceEventService,
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
ISendCoreHelperService sendCoreHelperService)
|
||||||
|
{
|
||||||
|
_sendRepository = sendRepository;
|
||||||
|
_sendFileStorageService = sendFileStorageService;
|
||||||
|
_pushNotificationService = pushNotificationService;
|
||||||
|
_sendValidationService = sendValidationService;
|
||||||
|
_referenceEventService = referenceEventService;
|
||||||
|
_currentContext = currentContext;
|
||||||
|
_sendCoreHelperService = sendCoreHelperService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveSendAsync(Send send)
|
||||||
|
{
|
||||||
|
// Make sure user can save Sends
|
||||||
|
await _sendValidationService.ValidateUserCanSaveAsync(send.UserId, send);
|
||||||
|
|
||||||
|
if (send.Id == default(Guid))
|
||||||
|
{
|
||||||
|
await _sendRepository.CreateAsync(send);
|
||||||
|
await _pushNotificationService.PushSyncSendCreateAsync(send);
|
||||||
|
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
|
||||||
|
{
|
||||||
|
Id = send.UserId ?? default,
|
||||||
|
Type = ReferenceEventType.SendCreated,
|
||||||
|
Source = ReferenceEventSource.User,
|
||||||
|
SendType = send.Type,
|
||||||
|
MaxAccessCount = send.MaxAccessCount,
|
||||||
|
HasPassword = !string.IsNullOrWhiteSpace(send.Password),
|
||||||
|
SendHasNotes = send.Data?.Contains("Notes"),
|
||||||
|
ClientId = _currentContext.ClientId,
|
||||||
|
ClientVersion = _currentContext.ClientVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
send.RevisionDate = DateTime.UtcNow;
|
||||||
|
await _sendRepository.UpsertAsync(send);
|
||||||
|
await _pushNotificationService.PushSyncSendUpdateAsync(send);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength)
|
||||||
|
{
|
||||||
|
if (send.Type != SendType.File)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Send is not of type \"file\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileLength < 1)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("No file data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var storageBytesRemaining = await _sendValidationService.StorageRemainingForSendAsync(send);
|
||||||
|
|
||||||
|
if (storageBytesRemaining < fileLength)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Not enough storage available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileId = _sendCoreHelperService.SecureRandomString(32, useUpperCase: false, useSpecial: false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data.Id = fileId;
|
||||||
|
data.Size = fileLength;
|
||||||
|
data.Validated = false;
|
||||||
|
send.Data = JsonSerializer.Serialize(data,
|
||||||
|
JsonHelpers.IgnoreWritingNull);
|
||||||
|
await SaveSendAsync(send);
|
||||||
|
return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Clean up since this is not transactional
|
||||||
|
await _sendFileStorageService.DeleteFileAsync(send, fileId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async Task UploadFileToExistingSendAsync(Stream stream, Send send)
|
||||||
|
{
|
||||||
|
if (stream.Position > 0)
|
||||||
|
{
|
||||||
|
stream.Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (send?.Data == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Send does not have file data");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (send.Type != SendType.File)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Not a File Type Send.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||||
|
|
||||||
|
if (data.Validated)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("File has already been uploaded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _sendFileStorageService.UploadNewFileAsync(stream, send, data.Id);
|
||||||
|
|
||||||
|
if (!await ConfirmFileSize(send))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("File received does not match expected file length.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async Task DeleteSendAsync(Send send)
|
||||||
|
{
|
||||||
|
await _sendRepository.DeleteAsync(send);
|
||||||
|
if (send.Type == Enums.SendType.File)
|
||||||
|
{
|
||||||
|
var data = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||||
|
await _sendFileStorageService.DeleteFileAsync(send, data.Id);
|
||||||
|
}
|
||||||
|
await _pushNotificationService.PushSyncSendDeleteAsync(send);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ConfirmFileSize(Send send)
|
||||||
|
{
|
||||||
|
var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||||
|
|
||||||
|
var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY);
|
||||||
|
|
||||||
|
if (!valid || realSize > SendFileSettingHelper.FILE_SIZE_LEEWAY)
|
||||||
|
{
|
||||||
|
// File reported differs in size from that promised. Must be a rogue client. Delete Send
|
||||||
|
await DeleteSendAsync(send);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Send data if necessary
|
||||||
|
if (realSize != fileData.Size)
|
||||||
|
{
|
||||||
|
fileData.Size = realSize.Value;
|
||||||
|
}
|
||||||
|
fileData.Validated = true;
|
||||||
|
send.Data = JsonSerializer.Serialize(fileData,
|
||||||
|
JsonHelpers.IgnoreWritingNull);
|
||||||
|
await SaveSendAsync(send);
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
using Bit.Core.Tools.SendFeatures.Commands;
|
||||||
|
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
|
using Bit.Core.Tools.Services;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.SendFeatures;
|
||||||
|
|
||||||
|
public static class SendServiceCollectionExtension
|
||||||
|
{
|
||||||
|
public static void AddSendServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<INonAnonymousSendCommand, NonAnonymousSendCommand>();
|
||||||
|
services.AddScoped<IAnonymousSendCommand, AnonymousSendCommand>();
|
||||||
|
services.AddScoped<ISendAuthorizationService, SendAuthorizationService>();
|
||||||
|
services.AddScoped<ISendValidationService, SendValidationService>();
|
||||||
|
services.AddScoped<ISendCoreHelperService, SendCoreHelperService>();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
using Bit.Core.Tools.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send Authorization service is responsible for checking if a Send can be accessed.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISendAuthorizationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a <see cref="Send" /> can be accessed while updating the <see cref="Send" />, pushing a notification, and sending a reference event.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send"><see cref="Send" /> used to determine access</param>
|
||||||
|
/// <param name="password">A hashed and base64-encoded password. This is compared with the send's password to authorize access.</param>
|
||||||
|
/// <returns><see cref="SendAccessResult" /> will be returned to determine if the user can access send.
|
||||||
|
/// </returns>
|
||||||
|
Task<SendAccessResult> AccessAsync(Send send, string password);
|
||||||
|
SendAccessResult SendCanBeAccessed(Send send,
|
||||||
|
string password);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hashes the password using the password hasher.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="password">Password to be hashed</param>
|
||||||
|
/// <returns>Hashed password of the password given</returns>
|
||||||
|
string HashPassword(string password);
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
namespace Bit.Core.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This interface provides helper methods for generating secure random strings. Making
|
||||||
|
/// it easier to mock the service in unit tests.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISendCoreHelperService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Securely generates a random string of the specified length.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="length">Desired string length to be returned</param>
|
||||||
|
/// <param name="useUpperCase">Desired casing for the string</param>
|
||||||
|
/// <param name="useSpecial">Determines if special characters will be used in string</param>
|
||||||
|
/// <returns>A secure random string with the desired parameters</returns>
|
||||||
|
string SecureRandomString(int length, bool useUpperCase, bool useSpecial);
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send File Storage Service is responsible for uploading, deleting, and validating files
|
||||||
|
/// whether they are in local storage or in cloud storage.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISendFileStorageService
|
||||||
|
{
|
||||||
|
FileUploadType FileUploadType { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Uploads a new file to the storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream"><see cref="Stream" /> of the file</param>
|
||||||
|
/// <param name="send"><see cref="Send" /> for the file</param>
|
||||||
|
/// <param name="fileId">File id</param>
|
||||||
|
/// <returns>Task completes once <see cref="Stream" /> and <see cref="Send" /> have been saved to the database</returns>
|
||||||
|
Task UploadNewFileAsync(Stream stream, Send send, string fileId);
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a file from the storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send"><see cref="Send" /> used to delete file</param>
|
||||||
|
/// <param name="fileId">File id of file to be deleted</param>
|
||||||
|
/// <returns>Task completes once <see cref="Send" /> has been deleted to the database</returns>
|
||||||
|
Task DeleteFileAsync(Send send, string fileId);
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes all files for a specific organization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId"><see cref="Guid" /> used to delete all files pertaining to organization</param>
|
||||||
|
/// <returns>Task completes after running code to delete files by organization id</returns>
|
||||||
|
Task DeleteFilesForOrganizationAsync(Guid organizationId);
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes all files for a specific user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"><see cref="Guid" /> used to delete all files pertaining to user</param>
|
||||||
|
/// <returns>Task completes after running code to delete files by user id</returns>
|
||||||
|
Task DeleteFilesForUserAsync(Guid userId);
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the download URL for a file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send"><see cref="Send" /> used to help get download url for file</param>
|
||||||
|
/// <param name="fileId">File id to help get download url for file</param>
|
||||||
|
/// <returns>Download url as a string</returns>
|
||||||
|
Task<string> GetSendFileDownloadUrlAsync(Send send, string fileId);
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the upload URL for a file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send"><see cref="Send" /> used to help get upload url for file </param>
|
||||||
|
/// <param name="fileId">File id to help get upload url for file</param>
|
||||||
|
/// <returns>File upload url as string</returns>
|
||||||
|
Task<string> GetSendFileUploadUrlAsync(Send send, string fileId);
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the file size of a file in the storage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send"><see cref="Send" /> used to help validate file</param>
|
||||||
|
/// <param name="fileId">File id to identify which file to validate</param>
|
||||||
|
/// <param name="expectedFileSize">Expected file size of the file</param>
|
||||||
|
/// <param name="leeway">
|
||||||
|
/// Send file size tolerance in bytes. When an uploaded file's `expectedFileSize`
|
||||||
|
/// is outside of the leeway, the storage operation fails.
|
||||||
|
/// </param>
|
||||||
|
/// <throws>
|
||||||
|
/// ❌ Fill this in with an explanation of the error thrown when `leeway` is incorrect
|
||||||
|
/// </throws>
|
||||||
|
/// <returns>Task object for async operations with Tuple of boolean that determines if file was valid and long that
|
||||||
|
/// the actual file size of the file.
|
||||||
|
/// </returns>
|
||||||
|
Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway);
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.Services;
|
||||||
|
|
||||||
|
public interface ISendValidationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates a file can be saved by specified user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"><see cref="Guid" /> needed to validate file for specific user</param>
|
||||||
|
/// <param name="send"><see cref="Send" /> needed to help validate file</param>
|
||||||
|
/// <returns>Task completes when a conditional statement has been met it will return out of the method or
|
||||||
|
/// throw a BadRequestException.
|
||||||
|
/// </returns>
|
||||||
|
Task ValidateUserCanSaveAsync(Guid? userId, Send send);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates a file can be saved by specified user with different policy based on feature flag
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"><see cref="Guid" /> needed to validate file for specific user</param>
|
||||||
|
/// <param name="send"><see cref="Send" /> needed to help validate file</param>
|
||||||
|
/// <returns>Task completes when a conditional statement has been met it will return out of the method or
|
||||||
|
/// throw a BadRequestException.
|
||||||
|
/// </returns>
|
||||||
|
Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the remaining storage for a Send.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="send"><see cref="Send" /> needed to help calculate remaining storage</param>
|
||||||
|
/// <returns>Long with the remaining bytes for storage or will throw a BadRequestException if user cannot access
|
||||||
|
/// file or email is not verified.
|
||||||
|
/// </returns>
|
||||||
|
Task<long> StorageRemainingForSendAsync(Send send);
|
||||||
|
}
|
101
src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs
Normal file
101
src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
using Bit.Core.Tools.Enums;
|
||||||
|
using Bit.Core.Tools.Models.Business;
|
||||||
|
using Bit.Core.Tools.Models.Data;
|
||||||
|
using Bit.Core.Tools.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.Services;
|
||||||
|
|
||||||
|
public class SendAuthorizationService : ISendAuthorizationService
|
||||||
|
{
|
||||||
|
private readonly ISendRepository _sendRepository;
|
||||||
|
private readonly IPasswordHasher<User> _passwordHasher;
|
||||||
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
|
public SendAuthorizationService(
|
||||||
|
ISendRepository sendRepository,
|
||||||
|
IPasswordHasher<User> passwordHasher,
|
||||||
|
IPushNotificationService pushNotificationService,
|
||||||
|
IReferenceEventService referenceEventService,
|
||||||
|
ICurrentContext currentContext)
|
||||||
|
{
|
||||||
|
_sendRepository = sendRepository;
|
||||||
|
_passwordHasher = passwordHasher;
|
||||||
|
_pushNotificationService = pushNotificationService;
|
||||||
|
_referenceEventService = referenceEventService;
|
||||||
|
_currentContext = currentContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendAccessResult SendCanBeAccessed(Send send,
|
||||||
|
string password)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (send == null || send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount ||
|
||||||
|
send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < now || send.Disabled ||
|
||||||
|
send.DeletionDate < now)
|
||||||
|
{
|
||||||
|
return SendAccessResult.Denied;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(send.Password))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(password))
|
||||||
|
{
|
||||||
|
return SendAccessResult.PasswordRequired;
|
||||||
|
}
|
||||||
|
var passwordResult = _passwordHasher.VerifyHashedPassword(new User(), send.Password, password);
|
||||||
|
if (passwordResult == PasswordVerificationResult.SuccessRehashNeeded)
|
||||||
|
{
|
||||||
|
send.Password = HashPassword(password);
|
||||||
|
}
|
||||||
|
if (passwordResult == PasswordVerificationResult.Failed)
|
||||||
|
{
|
||||||
|
return SendAccessResult.PasswordInvalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendAccessResult.Granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SendAccessResult> AccessAsync(Send sendToBeAccessed, string password)
|
||||||
|
{
|
||||||
|
var accessResult = SendCanBeAccessed(sendToBeAccessed, password);
|
||||||
|
|
||||||
|
if (!accessResult.Equals(SendAccessResult.Granted))
|
||||||
|
{
|
||||||
|
return accessResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendToBeAccessed.Type != SendType.File)
|
||||||
|
{
|
||||||
|
// File sends are incremented during file download
|
||||||
|
sendToBeAccessed.AccessCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _sendRepository.ReplaceAsync(sendToBeAccessed);
|
||||||
|
await _pushNotificationService.PushSyncSendUpdateAsync(sendToBeAccessed);
|
||||||
|
await _referenceEventService.RaiseEventAsync(new ReferenceEvent
|
||||||
|
{
|
||||||
|
Id = sendToBeAccessed.UserId ?? default,
|
||||||
|
Type = ReferenceEventType.SendAccessed,
|
||||||
|
Source = ReferenceEventSource.User,
|
||||||
|
SendType = sendToBeAccessed.Type,
|
||||||
|
MaxAccessCount = sendToBeAccessed.MaxAccessCount,
|
||||||
|
HasPassword = !string.IsNullOrWhiteSpace(sendToBeAccessed.Password),
|
||||||
|
SendHasNotes = sendToBeAccessed.Data?.Contains("Notes"),
|
||||||
|
ClientId = _currentContext.ClientId,
|
||||||
|
ClientVersion = _currentContext.ClientVersion
|
||||||
|
});
|
||||||
|
return accessResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string HashPassword(string password)
|
||||||
|
{
|
||||||
|
return _passwordHasher.HashPassword(new User(), password);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.Services;
|
||||||
|
|
||||||
|
public class SendCoreHelperService : ISendCoreHelperService
|
||||||
|
{
|
||||||
|
public string SecureRandomString(int length, bool useUpperCase, bool useSpecial)
|
||||||
|
{
|
||||||
|
return CoreHelpers.SecureRandomString(length, upper: useUpperCase, special: useSpecial);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user