mirror of
https://github.com/bitwarden/server.git
synced 2025-04-07 05:58:13 -05:00
Merge branch 'main' into jmccannon/ac/pm-16811-scim-invite-optimization
# Conflicts: # src/Core/Models/Commands/CommandResult.cs
This commit is contained in:
commit
88923b5e6e
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.2.2</Version>
|
<Version>2025.2.4</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -43,8 +43,9 @@ public class PushController : Controller
|
|||||||
public async Task RegisterAsync([FromBody] PushRegistrationRequestModel model)
|
public async Task RegisterAsync([FromBody] PushRegistrationRequestModel model)
|
||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
await _pushRegistrationService.CreateOrUpdateRegistrationAsync(new PushRegistrationData(model.PushToken), Prefix(model.DeviceId),
|
await _pushRegistrationService.CreateOrUpdateRegistrationAsync(new PushRegistrationData(model.PushToken),
|
||||||
Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix), model.InstallationId);
|
Prefix(model.DeviceId), Prefix(model.UserId), Prefix(model.Identifier), model.Type,
|
||||||
|
model.OrganizationIds?.Select(Prefix) ?? [], model.InstallationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("delete")]
|
[HttpPost("delete")]
|
||||||
|
@ -56,7 +56,7 @@ public class ImportCiphersController : Controller
|
|||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList();
|
var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList();
|
||||||
var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList();
|
var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList();
|
||||||
await _importCiphersCommand.ImportIntoIndividualVaultAsync(folders, ciphers, model.FolderRelationships);
|
await _importCiphersCommand.ImportIntoIndividualVaultAsync(folders, ciphers, model.FolderRelationships, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("import-organization")]
|
[HttpPost("import-organization")]
|
||||||
|
31
src/Api/Utilities/CommandResultExtensions.cs
Normal file
31
src/Api/Utilities/CommandResultExtensions.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using Bit.Core.Models.Commands;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.Utilities;
|
||||||
|
|
||||||
|
public static class CommandResultExtensions
|
||||||
|
{
|
||||||
|
public static IActionResult MapToActionResult<T>(this CommandResult<T> commandResult)
|
||||||
|
{
|
||||||
|
return commandResult switch
|
||||||
|
{
|
||||||
|
NoRecordFoundFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound },
|
||||||
|
BadRequestFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
||||||
|
Failure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
||||||
|
Success<T> success => new ObjectResult(success.Data) { StatusCode = StatusCodes.Status200OK },
|
||||||
|
_ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IActionResult MapToActionResult(this CommandResult commandResult)
|
||||||
|
{
|
||||||
|
return commandResult switch
|
||||||
|
{
|
||||||
|
NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound },
|
||||||
|
BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
||||||
|
Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
||||||
|
Success => new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK },
|
||||||
|
_ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -22,4 +22,9 @@ public static class CustomerExtensions
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static bool HasTaxLocationVerified(this Customer customer) =>
|
public static bool HasTaxLocationVerified(this Customer customer) =>
|
||||||
customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
|
customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
|
||||||
|
|
||||||
|
public static decimal GetBillingBalance(this Customer customer)
|
||||||
|
{
|
||||||
|
return customer != null ? customer.Balance / 100M : default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
26
src/Core/Billing/Extensions/SubscriberExtensions.cs
Normal file
26
src/Core/Billing/Extensions/SubscriberExtensions.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Extensions;
|
||||||
|
|
||||||
|
public static class SubscriberExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// We are taking only first 30 characters of the SubscriberName because stripe provide for 30 characters for
|
||||||
|
/// custom_fields,see the link: https://stripe.com/docs/api/invoices/create
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscriber"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetFormattedInvoiceName(this ISubscriber subscriber)
|
||||||
|
{
|
||||||
|
var subscriberName = subscriber.SubscriberName();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(subscriberName))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscriberName.Length <= 30
|
||||||
|
? subscriberName
|
||||||
|
: subscriberName[..30];
|
||||||
|
}
|
||||||
|
}
|
@ -120,7 +120,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||||
public const string DuoRedirect = "duo-redirect";
|
public const string DuoRedirect = "duo-redirect";
|
||||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||||
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
|
||||||
public const string EmailVerification = "email-verification";
|
public const string EmailVerification = "email-verification";
|
||||||
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
||||||
public const string ExtensionRefresh = "extension-refresh";
|
public const string ExtensionRefresh = "extension-refresh";
|
||||||
|
@ -73,8 +73,11 @@ public class SubscriptionInfo
|
|||||||
Name = item.Plan.Nickname;
|
Name = item.Plan.Nickname;
|
||||||
Amount = item.Plan.Amount.GetValueOrDefault() / 100M;
|
Amount = item.Plan.Amount.GetValueOrDefault() / 100M;
|
||||||
Interval = item.Plan.Interval;
|
Interval = item.Plan.Interval;
|
||||||
AddonSubscriptionItem =
|
|
||||||
Utilities.StaticStore.IsAddonSubscriptionItem(item.Plan.Id);
|
if (item.Metadata != null)
|
||||||
|
{
|
||||||
|
AddonSubscriptionItem = item.Metadata.TryGetValue("isAddOn", out var value) && bool.Parse(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Quantity = (int)item.Quantity;
|
Quantity = (int)item.Quantity;
|
||||||
|
23
src/Core/Models/Commands/BadRequestFailure.cs
Normal file
23
src/Core/Models/Commands/BadRequestFailure.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace Bit.Core.Models.Commands;
|
||||||
|
|
||||||
|
public class BadRequestFailure<T> : Failure<T>
|
||||||
|
{
|
||||||
|
public BadRequestFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BadRequestFailure(string errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BadRequestFailure : Failure
|
||||||
|
{
|
||||||
|
public BadRequestFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BadRequestFailure(string errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
namespace Bit.Core.Models.Commands;
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Commands;
|
||||||
|
|
||||||
public class CommandResult(IEnumerable<string> errors)
|
public class CommandResult(IEnumerable<string> errors)
|
||||||
{
|
{
|
||||||
@ -10,20 +12,34 @@ public class CommandResult(IEnumerable<string> errors)
|
|||||||
public CommandResult() : this(Array.Empty<string>()) { }
|
public CommandResult() : this(Array.Empty<string>()) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class CommandResult<T> : CommandResult
|
public class Failure : CommandResult
|
||||||
{
|
{
|
||||||
|
protected Failure(IEnumerable<string> errorMessages) : base(errorMessages)
|
||||||
|
{
|
||||||
|
|
||||||
public T Value { get; set; }
|
}
|
||||||
|
public Failure(string errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Success<T> : CommandResult<T>
|
public class Success : CommandResult
|
||||||
{
|
{
|
||||||
public Success(T value) => Value = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Failure<T> : CommandResult<T>
|
public abstract class CommandResult<T> { }
|
||||||
|
|
||||||
|
public class Success<T>(T value) : CommandResult<T>
|
||||||
{
|
{
|
||||||
public Failure(string error) => ErrorMessages.Add(error);
|
public T? Value { get; } = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Failure<T>(IEnumerable<string> errorMessage) : CommandResult<T>
|
||||||
|
{
|
||||||
|
public List<string> ErrorMessages { get; } = [];
|
||||||
|
|
||||||
public string ErrorMessage => string.Join(" ", ErrorMessages);
|
public string ErrorMessage => string.Join(" ", ErrorMessages);
|
||||||
|
|
||||||
|
public Failure(string error) : this([error]) { }
|
||||||
}
|
}
|
||||||
|
24
src/Core/Models/Commands/NoRecordFoundFailure.cs
Normal file
24
src/Core/Models/Commands/NoRecordFoundFailure.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
namespace Bit.Core.Models.Commands;
|
||||||
|
|
||||||
|
public class NoRecordFoundFailure<T> : Failure<T>
|
||||||
|
{
|
||||||
|
public NoRecordFoundFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoRecordFoundFailure(string errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NoRecordFoundFailure : Failure
|
||||||
|
{
|
||||||
|
public NoRecordFoundFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoRecordFoundFailure(string errorMessage) : base(errorMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,18 +14,8 @@ namespace Bit.Core.Services;
|
|||||||
public interface IPaymentService
|
public interface IPaymentService
|
||||||
{
|
{
|
||||||
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
||||||
Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
|
||||||
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
|
|
||||||
bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0,
|
|
||||||
int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false);
|
|
||||||
Task<string> PurchaseOrganizationNoPaymentMethod(Organization org, Plan plan, int additionalSeats,
|
|
||||||
bool premiumAccessAddon, int additionalSmSeats = 0, int additionalServiceAccount = 0,
|
|
||||||
bool signupIsFromSecretsManagerTrial = false);
|
|
||||||
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
|
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||||
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
|
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||||
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
|
|
||||||
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
|
||||||
short additionalStorageGb, TaxInfo taxInfo);
|
|
||||||
Task<string> AdjustSubscription(
|
Task<string> AdjustSubscription(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
Plan updatedPlan,
|
Plan updatedPlan,
|
||||||
@ -56,9 +46,7 @@ public interface IPaymentService
|
|||||||
Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo);
|
Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo);
|
||||||
Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats,
|
Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats,
|
||||||
int additionalServiceAccount);
|
int additionalServiceAccount);
|
||||||
Task<bool> RisksSubscriptionFailure(Organization organization);
|
|
||||||
Task<bool> HasSecretsManagerStandalone(Organization organization);
|
Task<bool> HasSecretsManagerStandalone(Organization organization);
|
||||||
Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription);
|
|
||||||
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
||||||
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
||||||
|
|
||||||
|
@ -25,9 +25,6 @@ namespace Bit.Core.Services;
|
|||||||
|
|
||||||
public class StripePaymentService : IPaymentService
|
public class StripePaymentService : IPaymentService
|
||||||
{
|
{
|
||||||
private const string PremiumPlanId = "premium-annually";
|
|
||||||
private const string StoragePlanId = "storage-gb-annually";
|
|
||||||
private const string ProviderDiscountId = "msp-discount-35";
|
|
||||||
private const string SecretsManagerStandaloneDiscountId = "sm-standalone";
|
private const string SecretsManagerStandaloneDiscountId = "sm-standalone";
|
||||||
|
|
||||||
private readonly ITransactionRepository _transactionRepository;
|
private readonly ITransactionRepository _transactionRepository;
|
||||||
@ -62,240 +59,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
|
||||||
string paymentToken, StaticStore.Plan plan, short additionalStorageGb,
|
|
||||||
int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false,
|
|
||||||
int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false)
|
|
||||||
{
|
|
||||||
Braintree.Customer braintreeCustomer = null;
|
|
||||||
string stipeCustomerSourceToken = null;
|
|
||||||
string stipeCustomerPaymentMethodId = null;
|
|
||||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
|
||||||
};
|
|
||||||
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
|
|
||||||
paymentMethodType == PaymentMethodType.BankAccount;
|
|
||||||
|
|
||||||
if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken))
|
|
||||||
{
|
|
||||||
if (paymentToken.StartsWith("pm_"))
|
|
||||||
{
|
|
||||||
stipeCustomerPaymentMethodId = paymentToken;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
stipeCustomerSourceToken = paymentToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (paymentMethodType == PaymentMethodType.PayPal)
|
|
||||||
{
|
|
||||||
var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
|
||||||
var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest
|
|
||||||
{
|
|
||||||
PaymentMethodNonce = paymentToken,
|
|
||||||
Email = org.BillingEmail,
|
|
||||||
Id = org.BraintreeCustomerIdPrefix() + org.Id.ToString("N").ToLower() + randomSuffix,
|
|
||||||
CustomFields = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
[org.BraintreeIdField()] = org.Id.ToString(),
|
|
||||||
[org.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
|
|
||||||
{
|
|
||||||
throw new GatewayException("Failed to create PayPal customer record.");
|
|
||||||
}
|
|
||||||
|
|
||||||
braintreeCustomer = customerResult.Target;
|
|
||||||
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new GatewayException("Payment method is not supported at this time.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
|
|
||||||
, additionalSmSeats, additionalServiceAccount);
|
|
||||||
|
|
||||||
Customer customer = null;
|
|
||||||
Subscription subscription;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber))
|
|
||||||
{
|
|
||||||
taxInfo.TaxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry,
|
|
||||||
taxInfo.TaxIdNumber);
|
|
||||||
|
|
||||||
if (taxInfo.TaxIdType == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
|
||||||
taxInfo.BillingAddressCountry,
|
|
||||||
taxInfo.TaxIdNumber);
|
|
||||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var customerCreateOptions = new CustomerCreateOptions
|
|
||||||
{
|
|
||||||
Description = org.DisplayBusinessName(),
|
|
||||||
Email = org.BillingEmail,
|
|
||||||
Source = stipeCustomerSourceToken,
|
|
||||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
|
||||||
Metadata = stripeCustomerMetadata,
|
|
||||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
|
||||||
{
|
|
||||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
|
||||||
CustomFields =
|
|
||||||
[
|
|
||||||
new CustomerInvoiceSettingsCustomFieldOptions
|
|
||||||
{
|
|
||||||
Name = org.SubscriberType(),
|
|
||||||
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
Coupon = signupIsFromSecretsManagerTrial
|
|
||||||
? SecretsManagerStandaloneDiscountId
|
|
||||||
: provider
|
|
||||||
? ProviderDiscountId
|
|
||||||
: null,
|
|
||||||
Address = new AddressOptions
|
|
||||||
{
|
|
||||||
Country = taxInfo?.BillingAddressCountry,
|
|
||||||
PostalCode = taxInfo?.BillingAddressPostalCode,
|
|
||||||
// Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead.
|
|
||||||
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 }]
|
|
||||||
: null
|
|
||||||
};
|
|
||||||
|
|
||||||
customerCreateOptions.AddExpand("tax");
|
|
||||||
|
|
||||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
|
||||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
|
||||||
subCreateOptions.Customer = customer.Id;
|
|
||||||
subCreateOptions.EnableAutomaticTax(customer);
|
|
||||||
|
|
||||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
|
||||||
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
|
|
||||||
{
|
|
||||||
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
|
|
||||||
{
|
|
||||||
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions());
|
|
||||||
throw new GatewayException("Payment method was declined.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error creating customer, walking back operation.");
|
|
||||||
if (customer != null)
|
|
||||||
{
|
|
||||||
await _stripeAdapter.CustomerDeleteAsync(customer.Id);
|
|
||||||
}
|
|
||||||
if (braintreeCustomer != null)
|
|
||||||
{
|
|
||||||
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
|
||||||
}
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
org.Gateway = GatewayType.Stripe;
|
|
||||||
org.GatewayCustomerId = customer.Id;
|
|
||||||
org.GatewaySubscriptionId = subscription.Id;
|
|
||||||
|
|
||||||
if (subscription.Status == "incomplete" &&
|
|
||||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
|
||||||
{
|
|
||||||
org.Enabled = false;
|
|
||||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
org.Enabled = true;
|
|
||||||
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> PurchaseOrganizationNoPaymentMethod(Organization org, StaticStore.Plan plan, int additionalSeats, bool premiumAccessAddon,
|
|
||||||
int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false)
|
|
||||||
{
|
|
||||||
|
|
||||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
|
||||||
};
|
|
||||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, new TaxInfo(), additionalSeats, 0, premiumAccessAddon
|
|
||||||
, additionalSmSeats, additionalServiceAccount);
|
|
||||||
|
|
||||||
Customer customer = null;
|
|
||||||
Subscription subscription;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var customerCreateOptions = new CustomerCreateOptions
|
|
||||||
{
|
|
||||||
Description = org.DisplayBusinessName(),
|
|
||||||
Email = org.BillingEmail,
|
|
||||||
Metadata = stripeCustomerMetadata,
|
|
||||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
|
||||||
{
|
|
||||||
CustomFields =
|
|
||||||
[
|
|
||||||
new CustomerInvoiceSettingsCustomFieldOptions
|
|
||||||
{
|
|
||||||
Name = org.SubscriberType(),
|
|
||||||
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
Coupon = signupIsFromSecretsManagerTrial
|
|
||||||
? SecretsManagerStandaloneDiscountId
|
|
||||||
: null,
|
|
||||||
TaxIdData = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
|
||||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
|
||||||
subCreateOptions.Customer = customer.Id;
|
|
||||||
|
|
||||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error creating customer, walking back operation.");
|
|
||||||
if (customer != null)
|
|
||||||
{
|
|
||||||
await _stripeAdapter.CustomerDeleteAsync(customer.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
org.Gateway = GatewayType.Stripe;
|
|
||||||
org.GatewayCustomerId = customer.Id;
|
|
||||||
org.GatewaySubscriptionId = subscription.Id;
|
|
||||||
|
|
||||||
if (subscription.Status == "incomplete" &&
|
|
||||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
|
||||||
{
|
|
||||||
org.Enabled = false;
|
|
||||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
org.Enabled = true;
|
|
||||||
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ChangeOrganizationSponsorship(
|
private async Task ChangeOrganizationSponsorship(
|
||||||
Organization org,
|
Organization org,
|
||||||
OrganizationSponsorship sponsorship,
|
OrganizationSponsorship sponsorship,
|
||||||
@ -324,458 +87,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) =>
|
public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) =>
|
||||||
ChangeOrganizationSponsorship(org, sponsorship, false);
|
ChangeOrganizationSponsorship(org, sponsorship, false);
|
||||||
|
|
||||||
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan,
|
|
||||||
OrganizationUpgrade upgrade)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Organization already has a subscription.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var customerOptions = new CustomerGetOptions();
|
|
||||||
customerOptions.AddExpand("default_source");
|
|
||||||
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
|
||||||
customerOptions.AddExpand("tax");
|
|
||||||
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions);
|
|
||||||
if (customer == null)
|
|
||||||
{
|
|
||||||
throw new GatewayException("Could not find customer payment profile.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(upgrade.TaxInfo?.BillingAddressCountry) &&
|
|
||||||
!string.IsNullOrEmpty(upgrade.TaxInfo?.BillingAddressPostalCode))
|
|
||||||
{
|
|
||||||
var addressOptions = new AddressOptions
|
|
||||||
{
|
|
||||||
Country = upgrade.TaxInfo.BillingAddressCountry,
|
|
||||||
PostalCode = upgrade.TaxInfo.BillingAddressPostalCode,
|
|
||||||
// Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead.
|
|
||||||
Line1 = upgrade.TaxInfo.BillingAddressLine1 ?? string.Empty,
|
|
||||||
Line2 = upgrade.TaxInfo.BillingAddressLine2,
|
|
||||||
City = upgrade.TaxInfo.BillingAddressCity,
|
|
||||||
State = upgrade.TaxInfo.BillingAddressState,
|
|
||||||
};
|
|
||||||
var customerUpdateOptions = new CustomerUpdateOptions { Address = addressOptions };
|
|
||||||
customerUpdateOptions.AddExpand("default_source");
|
|
||||||
customerUpdateOptions.AddExpand("invoice_settings.default_payment_method");
|
|
||||||
customerUpdateOptions.AddExpand("tax");
|
|
||||||
customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade);
|
|
||||||
|
|
||||||
subCreateOptions.EnableAutomaticTax(customer);
|
|
||||||
|
|
||||||
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
|
|
||||||
|
|
||||||
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
|
|
||||||
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
|
|
||||||
org.GatewaySubscriptionId = subscription.Id;
|
|
||||||
|
|
||||||
if (subscription.Status == "incomplete" &&
|
|
||||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
|
||||||
{
|
|
||||||
org.Enabled = false;
|
|
||||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
org.Enabled = true;
|
|
||||||
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
|
|
||||||
Customer customer, SubscriptionCreateOptions subCreateOptions)
|
|
||||||
{
|
|
||||||
var stripePaymentMethod = false;
|
|
||||||
var paymentMethodType = PaymentMethodType.Credit;
|
|
||||||
var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId");
|
|
||||||
if (hasBtCustomerId)
|
|
||||||
{
|
|
||||||
paymentMethodType = PaymentMethodType.PayPal;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card")
|
|
||||||
{
|
|
||||||
paymentMethodType = PaymentMethodType.Card;
|
|
||||||
stripePaymentMethod = true;
|
|
||||||
}
|
|
||||||
else if (customer.DefaultSource != null)
|
|
||||||
{
|
|
||||||
if (customer.DefaultSource is Card || customer.DefaultSource is SourceCard)
|
|
||||||
{
|
|
||||||
paymentMethodType = PaymentMethodType.Card;
|
|
||||||
stripePaymentMethod = true;
|
|
||||||
}
|
|
||||||
else if (customer.DefaultSource is BankAccount || customer.DefaultSource is SourceAchDebit)
|
|
||||||
{
|
|
||||||
paymentMethodType = PaymentMethodType.BankAccount;
|
|
||||||
stripePaymentMethod = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var paymentMethod = GetLatestCardPaymentMethod(customer.Id);
|
|
||||||
if (paymentMethod != null)
|
|
||||||
{
|
|
||||||
paymentMethodType = PaymentMethodType.Card;
|
|
||||||
stripePaymentMethod = true;
|
|
||||||
subCreateOptions.DefaultPaymentMethod = paymentMethod.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (stripePaymentMethod, paymentMethodType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType,
|
|
||||||
string paymentToken, short additionalStorageGb, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
if (paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Payment token is required.");
|
|
||||||
}
|
|
||||||
if (paymentMethodType == PaymentMethodType.Credit &&
|
|
||||||
(user.Gateway != GatewayType.Stripe || string.IsNullOrWhiteSpace(user.GatewayCustomerId)))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Your account does not have any credit available.");
|
|
||||||
}
|
|
||||||
if (paymentMethodType is PaymentMethodType.BankAccount)
|
|
||||||
{
|
|
||||||
throw new GatewayException("Payment method is not supported at this time.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var createdStripeCustomer = false;
|
|
||||||
Customer customer = null;
|
|
||||||
Braintree.Customer braintreeCustomer = null;
|
|
||||||
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount
|
|
||||||
or PaymentMethodType.Credit;
|
|
||||||
|
|
||||||
string stipeCustomerPaymentMethodId = null;
|
|
||||||
string stipeCustomerSourceToken = null;
|
|
||||||
if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken))
|
|
||||||
{
|
|
||||||
if (paymentToken.StartsWith("pm_"))
|
|
||||||
{
|
|
||||||
stipeCustomerPaymentMethodId = paymentToken;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
stipeCustomerSourceToken = paymentToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.Gateway == GatewayType.Stripe && !string.IsNullOrWhiteSpace(user.GatewayCustomerId))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(paymentToken))
|
|
||||||
{
|
|
||||||
await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var customerGetOptions = new CustomerGetOptions();
|
|
||||||
customerGetOptions.AddExpand("tax");
|
|
||||||
customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId, customerGetOptions);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Attempted to get existing customer from Stripe, but customer ID was not found. Attempting to recreate customer...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customer == null && !string.IsNullOrWhiteSpace(paymentToken))
|
|
||||||
{
|
|
||||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
|
||||||
};
|
|
||||||
if (paymentMethodType == PaymentMethodType.PayPal)
|
|
||||||
{
|
|
||||||
var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
|
||||||
var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest
|
|
||||||
{
|
|
||||||
PaymentMethodNonce = paymentToken,
|
|
||||||
Email = user.Email,
|
|
||||||
Id = user.BraintreeCustomerIdPrefix() + user.Id.ToString("N").ToLower() + randomSuffix,
|
|
||||||
CustomFields = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
[user.BraintreeIdField()] = user.Id.ToString(),
|
|
||||||
[user.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
|
|
||||||
{
|
|
||||||
throw new GatewayException("Failed to create PayPal customer record.");
|
|
||||||
}
|
|
||||||
|
|
||||||
braintreeCustomer = customerResult.Target;
|
|
||||||
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
|
|
||||||
}
|
|
||||||
else if (!stripePaymentMethod)
|
|
||||||
{
|
|
||||||
throw new GatewayException("Payment method is not supported at this time.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var customerCreateOptions = new CustomerCreateOptions
|
|
||||||
{
|
|
||||||
Description = user.Name,
|
|
||||||
Email = user.Email,
|
|
||||||
Metadata = stripeCustomerMetadata,
|
|
||||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
|
||||||
Source = stipeCustomerSourceToken,
|
|
||||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
|
||||||
{
|
|
||||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
|
||||||
CustomFields =
|
|
||||||
[
|
|
||||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
|
||||||
{
|
|
||||||
Name = user.SubscriberType(),
|
|
||||||
Value = GetFirstThirtyCharacters(user.SubscriberName()),
|
|
||||||
}
|
|
||||||
|
|
||||||
]
|
|
||||||
},
|
|
||||||
Address = new AddressOptions
|
|
||||||
{
|
|
||||||
Line1 = string.Empty,
|
|
||||||
Country = taxInfo.BillingAddressCountry,
|
|
||||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
customerCreateOptions.AddExpand("tax");
|
|
||||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
|
||||||
createdStripeCustomer = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customer == null)
|
|
||||||
{
|
|
||||||
throw new GatewayException("Could not set up customer payment profile.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCreateOptions = new SubscriptionCreateOptions
|
|
||||||
{
|
|
||||||
Customer = customer.Id,
|
|
||||||
Items = [],
|
|
||||||
Metadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
[user.GatewayIdField()] = user.Id.ToString()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Plan = PremiumPlanId,
|
|
||||||
Quantity = 1
|
|
||||||
});
|
|
||||||
|
|
||||||
if (additionalStorageGb > 0)
|
|
||||||
{
|
|
||||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Plan = StoragePlanId,
|
|
||||||
Quantity = additionalStorageGb
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
subCreateOptions.EnableAutomaticTax(customer);
|
|
||||||
|
|
||||||
var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer,
|
|
||||||
stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer);
|
|
||||||
|
|
||||||
user.Gateway = GatewayType.Stripe;
|
|
||||||
user.GatewayCustomerId = customer.Id;
|
|
||||||
user.GatewaySubscriptionId = subscription.Id;
|
|
||||||
|
|
||||||
if (subscription.Status == "incomplete" &&
|
|
||||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
|
||||||
{
|
|
||||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Premium = true;
|
|
||||||
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Subscription> ChargeForNewSubscriptionAsync(ISubscriber subscriber, Customer customer,
|
|
||||||
bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType,
|
|
||||||
SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
|
|
||||||
{
|
|
||||||
var addedCreditToStripeCustomer = false;
|
|
||||||
Braintree.Transaction braintreeTransaction = null;
|
|
||||||
|
|
||||||
var subInvoiceMetadata = new Dictionary<string, string>();
|
|
||||||
Subscription subscription = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!stripePaymentMethod)
|
|
||||||
{
|
|
||||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
|
|
||||||
{
|
|
||||||
Customer = customer.Id,
|
|
||||||
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (customer.HasTaxLocationVerified())
|
|
||||||
{
|
|
||||||
previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previewInvoice.AmountDue > 0)
|
|
||||||
{
|
|
||||||
var braintreeCustomerId = customer.Metadata != null &&
|
|
||||||
customer.Metadata.ContainsKey("btCustomerId") ? customer.Metadata["btCustomerId"] : null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(braintreeCustomerId))
|
|
||||||
{
|
|
||||||
var btInvoiceAmount = (previewInvoice.AmountDue / 100M);
|
|
||||||
var transactionResult = await _btGateway.Transaction.SaleAsync(
|
|
||||||
new Braintree.TransactionRequest
|
|
||||||
{
|
|
||||||
Amount = btInvoiceAmount,
|
|
||||||
CustomerId = braintreeCustomerId,
|
|
||||||
Options = new Braintree.TransactionOptionsRequest
|
|
||||||
{
|
|
||||||
SubmitForSettlement = true,
|
|
||||||
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
|
||||||
{
|
|
||||||
CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id},{subscriber.BraintreeCloudRegionField()}:{_globalSettings.BaseServiceUri.CloudRegion}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
CustomFields = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
[subscriber.BraintreeIdField()] = subscriber.Id.ToString(),
|
|
||||||
[subscriber.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!transactionResult.IsSuccess())
|
|
||||||
{
|
|
||||||
throw new GatewayException("Failed to charge PayPal customer.");
|
|
||||||
}
|
|
||||||
|
|
||||||
braintreeTransaction = transactionResult.Target;
|
|
||||||
subInvoiceMetadata.Add("btTransactionId", braintreeTransaction.Id);
|
|
||||||
subInvoiceMetadata.Add("btPayPalTransactionId",
|
|
||||||
braintreeTransaction.PayPalDetails.AuthorizationId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new GatewayException("No payment was able to be collected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
|
||||||
{
|
|
||||||
Balance = customer.Balance - previewInvoice.AmountDue
|
|
||||||
});
|
|
||||||
addedCreditToStripeCustomer = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (paymentMethodType == PaymentMethodType.Credit)
|
|
||||||
{
|
|
||||||
var upcomingInvoiceOptions = new UpcomingInvoiceOptions
|
|
||||||
{
|
|
||||||
Customer = customer.Id,
|
|
||||||
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items),
|
|
||||||
SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates,
|
|
||||||
};
|
|
||||||
|
|
||||||
upcomingInvoiceOptions.EnableAutomaticTax(customer, null);
|
|
||||||
|
|
||||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions);
|
|
||||||
|
|
||||||
if (previewInvoice.AmountDue > 0)
|
|
||||||
{
|
|
||||||
throw new GatewayException("Your account does not have enough credit available.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subCreateOptions.OffSession = true;
|
|
||||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
|
||||||
|
|
||||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
|
||||||
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
|
|
||||||
{
|
|
||||||
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
|
|
||||||
{
|
|
||||||
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions());
|
|
||||||
throw new GatewayException("Payment method was declined.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stripePaymentMethod && subInvoiceMetadata.Any())
|
|
||||||
{
|
|
||||||
var invoices = await _stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions
|
|
||||||
{
|
|
||||||
Subscription = subscription.Id
|
|
||||||
});
|
|
||||||
|
|
||||||
var invoice = invoices?.FirstOrDefault();
|
|
||||||
if (invoice == null)
|
|
||||||
{
|
|
||||||
throw new GatewayException("Invoice not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions
|
|
||||||
{
|
|
||||||
Metadata = subInvoiceMetadata
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return subscription;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
if (customer != null)
|
|
||||||
{
|
|
||||||
if (createdStripeCustomer)
|
|
||||||
{
|
|
||||||
await _stripeAdapter.CustomerDeleteAsync(customer.Id);
|
|
||||||
}
|
|
||||||
else if (addedCreditToStripeCustomer || customer.Balance < 0)
|
|
||||||
{
|
|
||||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
|
||||||
{
|
|
||||||
Balance = customer.Balance
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (braintreeTransaction != null)
|
|
||||||
{
|
|
||||||
await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id);
|
|
||||||
}
|
|
||||||
if (braintreeCustomer != null)
|
|
||||||
{
|
|
||||||
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e is StripeException strEx &&
|
|
||||||
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
|
||||||
{
|
|
||||||
throw new GatewayException("Bank account is not yet verified.");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
|
||||||
List<SubscriptionItemOptions> subItemOptions)
|
|
||||||
{
|
|
||||||
return subItemOptions.Select(si => new InvoiceSubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Plan = si.Plan,
|
|
||||||
Price = si.Price,
|
|
||||||
Quantity = si.Quantity,
|
|
||||||
Id = si.Id
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> FinalizeSubscriptionChangeAsync(ISubscriber subscriber,
|
private async Task<string> FinalizeSubscriptionChangeAsync(ISubscriber subscriber,
|
||||||
SubscriptionUpdate subscriptionUpdate, bool invoiceNow = false)
|
SubscriptionUpdate subscriptionUpdate, bool invoiceNow = false)
|
||||||
{
|
{
|
||||||
@ -1400,7 +711,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||||
{
|
{
|
||||||
Name = subscriber.SubscriberType(),
|
Name = subscriber.SubscriberType(),
|
||||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()),
|
Value = subscriber.GetFormattedInvoiceName()
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
@ -1492,7 +803,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||||
{
|
{
|
||||||
Name = subscriber.SubscriberType(),
|
Name = subscriber.SubscriberType(),
|
||||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName())
|
Value = subscriber.GetFormattedInvoiceName()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -1560,7 +871,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions());
|
var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions());
|
||||||
var billingInfo = new BillingInfo
|
var billingInfo = new BillingInfo
|
||||||
{
|
{
|
||||||
Balance = GetBillingBalance(customer),
|
Balance = customer.GetBillingBalance(),
|
||||||
PaymentSource = await GetBillingPaymentSourceAsync(customer)
|
PaymentSource = await GetBillingPaymentSourceAsync(customer)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1609,8 +920,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub);
|
subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub);
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.AC1795_UpdatedSubscriptionStatusSection))
|
|
||||||
{
|
|
||||||
var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(sub);
|
var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(sub);
|
||||||
|
|
||||||
if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue)
|
if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue)
|
||||||
@ -1619,7 +928,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate;
|
subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (sub is { CanceledAt: not null } || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
if (sub is { CanceledAt: not null } || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
@ -1771,27 +1079,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount),
|
new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount),
|
||||||
true);
|
true);
|
||||||
|
|
||||||
public async Task<bool> RisksSubscriptionFailure(Organization organization)
|
|
||||||
{
|
|
||||||
var subscriptionInfo = await GetSubscriptionAsync(organization);
|
|
||||||
|
|
||||||
if (subscriptionInfo.Subscription is not
|
|
||||||
{
|
|
||||||
Status: "active" or "trialing" or "past_due",
|
|
||||||
CollectionMethod: "charge_automatically"
|
|
||||||
}
|
|
||||||
|| subscriptionInfo.UpcomingInvoice == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var customer = await GetCustomerAsync(organization.GatewayCustomerId, GetCustomerPaymentOptions());
|
|
||||||
|
|
||||||
var paymentSource = await GetBillingPaymentSourceAsync(customer);
|
|
||||||
|
|
||||||
return paymentSource == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> HasSecretsManagerStandalone(Organization organization)
|
public async Task<bool> HasSecretsManagerStandalone(Organization organization)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
|
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||||
@ -1804,7 +1091,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId;
|
return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription)
|
private async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription)
|
||||||
{
|
{
|
||||||
if (subscription.Status is not "past_due" && subscription.Status is not "unpaid")
|
if (subscription.Status is not "past_due" && subscription.Status is not "unpaid")
|
||||||
{
|
{
|
||||||
@ -2120,11 +1407,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
|
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
private decimal GetBillingBalance(Customer customer)
|
|
||||||
{
|
|
||||||
return customer != null ? customer.Balance / 100M : default;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Customer customer)
|
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Customer customer)
|
||||||
{
|
{
|
||||||
if (customer == null)
|
if (customer == null)
|
||||||
@ -2255,18 +1537,4 @@ public class StripePaymentService : IPaymentService
|
|||||||
throw new GatewayException("Failed to retrieve current invoices", exception);
|
throw new GatewayException("Failed to retrieve current invoices", exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We are taking only first 30 characters of the SubscriberName because stripe provide
|
|
||||||
// for 30 characters for custom_fields,see the link: https://stripe.com/docs/api/invoices/create
|
|
||||||
private static string GetFirstThirtyCharacters(string subscriberName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(subscriberName))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return subscriberName.Length <= 30
|
|
||||||
? subscriberName
|
|
||||||
: subscriberName[..30];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -54,12 +54,11 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
public async Task ImportIntoIndividualVaultAsync(
|
public async Task ImportIntoIndividualVaultAsync(
|
||||||
List<Folder> folders,
|
List<Folder> folders,
|
||||||
List<CipherDetails> ciphers,
|
List<CipherDetails> ciphers,
|
||||||
IEnumerable<KeyValuePair<int, int>> folderRelationships)
|
IEnumerable<KeyValuePair<int, int>> folderRelationships,
|
||||||
|
Guid importingUserId)
|
||||||
{
|
{
|
||||||
var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId;
|
|
||||||
|
|
||||||
// Make sure the user can save new ciphers to their personal vault
|
// Make sure the user can save new ciphers to their personal vault
|
||||||
var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, PolicyType.PersonalOwnership);
|
var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership);
|
||||||
if (anyPersonalOwnershipPolicies)
|
if (anyPersonalOwnershipPolicies)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("You cannot import items into your personal vault because you are " +
|
throw new BadRequestException("You cannot import items into your personal vault because you are " +
|
||||||
@ -76,7 +75,7 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(userId ?? Guid.Empty)).Select(f => f.Id).ToList();
|
var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(importingUserId)).Select(f => f.Id).ToList();
|
||||||
|
|
||||||
//Assign id to the ones that don't exist in DB
|
//Assign id to the ones that don't exist in DB
|
||||||
//Need to keep the list order to create the relationships
|
//Need to keep the list order to create the relationships
|
||||||
@ -109,10 +108,7 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
await _cipherRepository.CreateAsync(ciphers, newFolders);
|
await _cipherRepository.CreateAsync(ciphers, newFolders);
|
||||||
|
|
||||||
// push
|
// push
|
||||||
if (userId.HasValue)
|
await _pushService.PushSyncVaultAsync(importingUserId);
|
||||||
{
|
|
||||||
await _pushService.PushSyncVaultAsync(userId.Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ImportIntoOrganizationalVaultAsync(
|
public async Task ImportIntoOrganizationalVaultAsync(
|
||||||
|
@ -7,7 +7,7 @@ namespace Bit.Core.Tools.ImportFeatures.Interfaces;
|
|||||||
public interface IImportCiphersCommand
|
public interface IImportCiphersCommand
|
||||||
{
|
{
|
||||||
Task ImportIntoIndividualVaultAsync(List<Folder> folders, List<CipherDetails> ciphers,
|
Task ImportIntoIndividualVaultAsync(List<Folder> folders, List<CipherDetails> ciphers,
|
||||||
IEnumerable<KeyValuePair<int, int>> folderRelationships);
|
IEnumerable<KeyValuePair<int, int>> folderRelationships, Guid importingUserId);
|
||||||
|
|
||||||
Task ImportIntoOrganizationalVaultAsync(List<Collection> collections, List<CipherDetails> ciphers,
|
Task ImportIntoOrganizationalVaultAsync(List<Collection> collections, List<CipherDetails> ciphers,
|
||||||
IEnumerable<KeyValuePair<int, int>> collectionRelationships, Guid importingUserId);
|
IEnumerable<KeyValuePair<int, int>> collectionRelationships, Guid importingUserId);
|
||||||
|
@ -158,21 +158,4 @@ public static class StaticStore
|
|||||||
|
|
||||||
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>
|
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>
|
||||||
SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType);
|
SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if the stripe plan id is an addon item by checking if the provided stripe plan id
|
|
||||||
/// matches either the <see cref="Plan.PasswordManagerPlanFeatures.StripeStoragePlanId"/> or <see cref="Plan.SecretsManagerPlanFeatures.StripeServiceAccountPlanId"/>
|
|
||||||
/// in any <see cref="Plans"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stripePlanId"></param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the stripePlanId is a addon product, false otherwise
|
|
||||||
/// </returns>
|
|
||||||
public static bool IsAddonSubscriptionItem(string stripePlanId)
|
|
||||||
{
|
|
||||||
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-16844
|
|
||||||
return Plans.Any(p =>
|
|
||||||
p.PasswordManager.StripeStoragePlanId == stripePlanId ||
|
|
||||||
(p.SecretsManager?.StripeServiceAccountPlanId == stripePlanId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -290,22 +290,34 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
|
|
||||||
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId)
|
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
{
|
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
var query = from u in dbContext.Users
|
var userQuery = from u in dbContext.Users
|
||||||
join ou in dbContext.OrganizationUsers on u.Id equals ou.UserId
|
|
||||||
join o in dbContext.Organizations on ou.OrganizationId equals o.Id
|
|
||||||
join od in dbContext.OrganizationDomains on ou.OrganizationId equals od.OrganizationId
|
|
||||||
where u.Id == userId
|
where u.Id == userId
|
||||||
&& od.VerifiedDate != null
|
select u;
|
||||||
&& u.Email.ToLower().EndsWith("@" + od.DomainName.ToLower())
|
|
||||||
|
var user = await userQuery.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
return new List<Core.AdminConsole.Entities.Organization>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var userWithDomain = new { UserId = user.Id, EmailDomain = user.Email.Split('@').Last() };
|
||||||
|
|
||||||
|
var query = from o in dbContext.Organizations
|
||||||
|
join ou in dbContext.OrganizationUsers on o.Id equals ou.OrganizationId
|
||||||
|
join od in dbContext.OrganizationDomains on ou.OrganizationId equals od.OrganizationId
|
||||||
|
where ou.UserId == userWithDomain.UserId &&
|
||||||
|
od.DomainName == userWithDomain.EmailDomain &&
|
||||||
|
od.VerifiedDate != null &&
|
||||||
|
o.Enabled == true
|
||||||
select o;
|
select o;
|
||||||
|
|
||||||
return await query.ToArrayAsync();
|
return await query.ToArrayAsync();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetAddableToProviderByUserIdAsync(
|
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetAddableToProviderByUserIdAsync(
|
||||||
Guid userId,
|
Guid userId,
|
||||||
|
@ -4,12 +4,19 @@ AS
|
|||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON;
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
SELECT O.*
|
WITH CTE_User AS (
|
||||||
FROM [dbo].[UserView] U
|
SELECT
|
||||||
INNER JOIN [dbo].[OrganizationUserView] OU ON U.[Id] = OU.[UserId]
|
U.*,
|
||||||
INNER JOIN [dbo].[OrganizationView] O ON OU.[OrganizationId] = O.[Id]
|
SUBSTRING(U.Email, CHARINDEX('@', U.Email) + 1, LEN(U.Email)) AS EmailDomain
|
||||||
INNER JOIN [dbo].[OrganizationDomainView] OD ON OU.[OrganizationId] = OD.[OrganizationId]
|
FROM dbo.[UserView] U
|
||||||
WHERE U.[Id] = @UserId
|
WHERE U.[Id] = @UserId
|
||||||
AND OD.[VerifiedDate] IS NOT NULL
|
)
|
||||||
AND U.[Email] LIKE '%@' + OD.[DomainName];
|
SELECT O.*
|
||||||
|
FROM CTE_User CU
|
||||||
|
INNER JOIN dbo.[OrganizationUserView] OU ON CU.[Id] = OU.[UserId]
|
||||||
|
INNER JOIN dbo.[OrganizationView] O ON OU.[OrganizationId] = O.[Id]
|
||||||
|
INNER JOIN dbo.[OrganizationDomainView] OD ON OU.[OrganizationId] = OD.[OrganizationId]
|
||||||
|
WHERE OD.[VerifiedDate] IS NOT NULL
|
||||||
|
AND CU.EmailDomain = OD.[DomainName]
|
||||||
|
AND O.[Enabled] = 1
|
||||||
END
|
END
|
||||||
|
@ -22,3 +22,8 @@ CREATE NONCLUSTERED INDEX [IX_OrganizationDomain_VerifiedDate]
|
|||||||
ON [dbo].[OrganizationDomain] ([VerifiedDate])
|
ON [dbo].[OrganizationDomain] ([VerifiedDate])
|
||||||
INCLUDE ([OrganizationId],[DomainName]);
|
INCLUDE ([OrganizationId],[DomainName]);
|
||||||
GO
|
GO
|
||||||
|
|
||||||
|
CREATE NONCLUSTERED INDEX [IX_OrganizationDomain_DomainNameVerifiedDateOrganizationId]
|
||||||
|
ON [dbo].[OrganizationDomain] ([DomainName],[VerifiedDate])
|
||||||
|
INCLUDE ([OrganizationId])
|
||||||
|
GO
|
||||||
|
@ -243,20 +243,22 @@ public class PushControllerTests
|
|||||||
PushToken = "test-push-token",
|
PushToken = "test-push-token",
|
||||||
UserId = userId.ToString(),
|
UserId = userId.ToString(),
|
||||||
Type = DeviceType.Android,
|
Type = DeviceType.Android,
|
||||||
Identifier = identifier.ToString()
|
Identifier = identifier.ToString(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Assert.Equal("Not correctly configured for push relays.", exception.Message);
|
Assert.Equal("Not correctly configured for push relays.", exception.Message);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IPushRegistrationService>().Received(0)
|
await sutProvider.GetDependency<IPushRegistrationService>().Received(0)
|
||||||
.CreateOrUpdateRegistrationAsync(Arg.Any<PushRegistrationData>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
.CreateOrUpdateRegistrationAsync(Arg.Any<PushRegistrationData>(), Arg.Any<string>(), Arg.Any<string>(),
|
||||||
Arg.Any<DeviceType>(), Arg.Any<IEnumerable<string>>(), Arg.Any<Guid>());
|
Arg.Any<string>(), Arg.Any<DeviceType>(), Arg.Any<IEnumerable<string>>(), Arg.Any<Guid>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData(false)]
|
||||||
public async Task? RegisterAsync_ValidModel_CreatedOrUpdatedRegistration(SutProvider<PushController> sutProvider,
|
[BitAutoData(true)]
|
||||||
Guid installationId, Guid userId, Guid identifier, Guid deviceId, Guid organizationId)
|
public async Task RegisterAsync_ValidModel_CreatedOrUpdatedRegistration(bool haveOrganizationId,
|
||||||
|
SutProvider<PushController> sutProvider, Guid installationId, Guid userId, Guid identifier, Guid deviceId,
|
||||||
|
Guid organizationId)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
||||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
||||||
@ -273,19 +275,29 @@ public class PushControllerTests
|
|||||||
UserId = userId.ToString(),
|
UserId = userId.ToString(),
|
||||||
Type = DeviceType.Android,
|
Type = DeviceType.Android,
|
||||||
Identifier = identifier.ToString(),
|
Identifier = identifier.ToString(),
|
||||||
OrganizationIds = [organizationId.ToString()],
|
OrganizationIds = haveOrganizationId ? [organizationId.ToString()] : null,
|
||||||
InstallationId = installationId
|
InstallationId = installationId
|
||||||
};
|
};
|
||||||
|
|
||||||
await sutProvider.Sut.RegisterAsync(model);
|
await sutProvider.Sut.RegisterAsync(model);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IPushRegistrationService>().Received(1)
|
await sutProvider.GetDependency<IPushRegistrationService>().Received(1)
|
||||||
.CreateOrUpdateRegistrationAsync(Arg.Is<PushRegistrationData>(data => data == new PushRegistrationData(model.PushToken)), expectedDeviceId, expectedUserId,
|
.CreateOrUpdateRegistrationAsync(
|
||||||
|
Arg.Is<PushRegistrationData>(data => data == new PushRegistrationData(model.PushToken)),
|
||||||
|
expectedDeviceId, expectedUserId,
|
||||||
expectedIdentifier, DeviceType.Android, Arg.Do<IEnumerable<string>>(organizationIds =>
|
expectedIdentifier, DeviceType.Android, Arg.Do<IEnumerable<string>>(organizationIds =>
|
||||||
{
|
{
|
||||||
|
Assert.NotNull(organizationIds);
|
||||||
var organizationIdsList = organizationIds.ToList();
|
var organizationIdsList = organizationIds.ToList();
|
||||||
|
if (haveOrganizationId)
|
||||||
|
{
|
||||||
Assert.Contains(expectedOrganizationId, organizationIdsList);
|
Assert.Contains(expectedOrganizationId, organizationIdsList);
|
||||||
Assert.Single(organizationIdsList);
|
Assert.Single(organizationIdsList);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Empty(organizationIdsList);
|
||||||
|
}
|
||||||
}), installationId);
|
}), installationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,8 @@ public class ImportCiphersControllerTests
|
|||||||
.ImportIntoIndividualVaultAsync(
|
.ImportIntoIndividualVaultAsync(
|
||||||
Arg.Any<List<Folder>>(),
|
Arg.Any<List<Folder>>(),
|
||||||
Arg.Any<List<CipherDetails>>(),
|
Arg.Any<List<CipherDetails>>(),
|
||||||
Arg.Any<IEnumerable<KeyValuePair<int, int>>>()
|
Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),
|
||||||
|
user.Id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
107
test/Api.Test/Utilities/CommandResultExtensionTests.cs
Normal file
107
test/Api.Test/Utilities/CommandResultExtensionTests.cs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
using Bit.Api.Utilities;
|
||||||
|
using Bit.Core.Models.Commands;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.Test.Utilities;
|
||||||
|
|
||||||
|
public class CommandResultExtensionTests
|
||||||
|
{
|
||||||
|
public static IEnumerable<object[]> WithGenericTypeTestCases()
|
||||||
|
{
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new NoRecordFoundFailure<Cipher>(new[] { "Error 1", "Error 2" }),
|
||||||
|
new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound }
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new BadRequestFailure<Cipher>("Error 3"),
|
||||||
|
new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest }
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new Failure<Cipher>("Error 4"),
|
||||||
|
new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest }
|
||||||
|
};
|
||||||
|
var cipher = new Cipher() { Id = Guid.NewGuid() };
|
||||||
|
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new Success<Cipher>(cipher),
|
||||||
|
new ObjectResult(cipher) { StatusCode = StatusCodes.Status200OK }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(WithGenericTypeTestCases))]
|
||||||
|
public void MapToActionResult_WithGenericType_ShouldMapToHttpResponse(CommandResult<Cipher> input, ObjectResult expected)
|
||||||
|
{
|
||||||
|
var result = input.MapToActionResult();
|
||||||
|
|
||||||
|
Assert.Equivalent(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MapToActionResult_WithGenericType_ShouldThrowExceptionForUnhandledCommandResult()
|
||||||
|
{
|
||||||
|
var result = new NotImplementedCommandResult();
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => result.MapToActionResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> TestCases()
|
||||||
|
{
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new NoRecordFoundFailure(new[] { "Error 1", "Error 2" }),
|
||||||
|
new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound }
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new BadRequestFailure("Error 3"),
|
||||||
|
new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest }
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new Failure("Error 4"),
|
||||||
|
new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest }
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new Success(),
|
||||||
|
new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(TestCases))]
|
||||||
|
public void MapToActionResult_ShouldMapToHttpResponse(CommandResult input, ObjectResult expected)
|
||||||
|
{
|
||||||
|
var result = input.MapToActionResult();
|
||||||
|
|
||||||
|
Assert.Equivalent(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MapToActionResult_ShouldThrowExceptionForUnhandledCommandResult()
|
||||||
|
{
|
||||||
|
var result = new NotImplementedCommandResult<Cipher>();
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => result.MapToActionResult());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotImplementedCommandResult<T> : CommandResult<T>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotImplementedCommandResult : CommandResult
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
23
test/Core.Test/Extensions/SubscriberExtensionsTests.cs
Normal file
23
test/Core.Test/Extensions/SubscriberExtensionsTests.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Extensions;
|
||||||
|
|
||||||
|
public class SubscriberExtensionsTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Alexandria Villanueva Gonzalez Pablo", "Alexandria Villanueva Gonzalez")]
|
||||||
|
[InlineData("John Snow", "John Snow")]
|
||||||
|
public void GetFormattedInvoiceName_Returns_FirstThirtyCaractersOfName(string name, string expected)
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var provider = new Provider { Name = name };
|
||||||
|
|
||||||
|
// act
|
||||||
|
var actual = provider.GetFormattedInvoiceName();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
@ -1,828 +0,0 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.Billing.Enums;
|
|
||||||
using Bit.Core.Billing.Services;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Exceptions;
|
|
||||||
using Bit.Core.Models.Business;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Settings;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Test.Common.AutoFixture;
|
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
|
||||||
using Braintree;
|
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
|
||||||
using Customer = Braintree.Customer;
|
|
||||||
using PaymentMethod = Braintree.PaymentMethod;
|
|
||||||
using PaymentMethodType = Bit.Core.Enums.PaymentMethodType;
|
|
||||||
|
|
||||||
namespace Bit.Core.Test.Services;
|
|
||||||
|
|
||||||
[SutProviderCustomize]
|
|
||||||
public class StripePaymentServiceTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(PaymentMethodType.BitPay)]
|
|
||||||
[BitAutoData(PaymentMethodType.BitPay)]
|
|
||||||
[BitAutoData(PaymentMethodType.Credit)]
|
|
||||||
[BitAutoData(PaymentMethodType.WireTransfer)]
|
|
||||||
[BitAutoData(PaymentMethodType.Check)]
|
|
||||||
public async Task PurchaseOrganizationAsync_Invalid(PaymentMethodType paymentMethodType, SutProvider<StripePaymentService> sutProvider)
|
|
||||||
{
|
|
||||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
|
||||||
() => sutProvider.Sut.PurchaseOrganizationAsync(null, paymentMethodType, null, null, 0, 0, false, null, false, -1, -1));
|
|
||||||
|
|
||||||
Assert.Equal("Payment method is not supported at this time.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_Stripe_ProviderOrg_Coupon_Add(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo, bool provider = true)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
|
|
||||||
sutProvider
|
|
||||||
.GetDependency<ITaxService>()
|
|
||||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
});
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>()
|
|
||||||
.BaseServiceUri.CloudRegion
|
|
||||||
.Returns("US");
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo, provider);
|
|
||||||
|
|
||||||
Assert.Null(result);
|
|
||||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
|
||||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
|
||||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
|
||||||
Assert.True(organization.Enabled);
|
|
||||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
|
||||||
|
|
||||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
|
||||||
c.Description == organization.BusinessName &&
|
|
||||||
c.Email == organization.BillingEmail &&
|
|
||||||
c.Source == paymentToken &&
|
|
||||||
c.PaymentMethod == null &&
|
|
||||||
c.Coupon == "msp-discount-35" &&
|
|
||||||
c.Metadata.Count == 1 &&
|
|
||||||
c.Metadata["region"] == "US" &&
|
|
||||||
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
|
||||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
|
||||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
|
||||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
|
||||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
|
||||||
c.Address.City == taxInfo.BillingAddressCity &&
|
|
||||||
c.Address.State == taxInfo.BillingAddressState &&
|
|
||||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
|
||||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
|
||||||
));
|
|
||||||
|
|
||||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
|
||||||
s.Customer == "C-1" &&
|
|
||||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
|
||||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
|
||||||
s.Items.Count == 0
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_SM_Stripe_ProviderOrg_Coupon_Add(SutProvider<StripePaymentService> sutProvider, Organization organization,
|
|
||||||
string paymentToken, TaxInfo taxInfo, bool provider = true)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
organization.UseSecretsManager = true;
|
|
||||||
|
|
||||||
sutProvider
|
|
||||||
.GetDependency<ITaxService>()
|
|
||||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
|
|
||||||
});
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>()
|
|
||||||
.BaseServiceUri.CloudRegion
|
|
||||||
.Returns("US");
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 1, 1,
|
|
||||||
false, taxInfo, provider, 1, 1);
|
|
||||||
|
|
||||||
Assert.Null(result);
|
|
||||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
|
||||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
|
||||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
|
||||||
Assert.True(organization.Enabled);
|
|
||||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
|
||||||
|
|
||||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
|
||||||
c.Description == organization.BusinessName &&
|
|
||||||
c.Email == organization.BillingEmail &&
|
|
||||||
c.Source == paymentToken &&
|
|
||||||
c.PaymentMethod == null &&
|
|
||||||
c.Coupon == "msp-discount-35" &&
|
|
||||||
c.Metadata.Count == 1 &&
|
|
||||||
c.Metadata["region"] == "US" &&
|
|
||||||
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
|
||||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
|
||||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
|
||||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
|
||||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
|
||||||
c.Address.City == taxInfo.BillingAddressCity &&
|
|
||||||
c.Address.State == taxInfo.BillingAddressState &&
|
|
||||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
|
||||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
|
||||||
));
|
|
||||||
|
|
||||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
|
||||||
s.Customer == "C-1" &&
|
|
||||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
|
||||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
|
||||||
s.Items.Count == 4
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_Stripe(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
organization.UseSecretsManager = true;
|
|
||||||
|
|
||||||
sutProvider
|
|
||||||
.GetDependency<ITaxService>()
|
|
||||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
});
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>()
|
|
||||||
.BaseServiceUri.CloudRegion
|
|
||||||
.Returns("US");
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0
|
|
||||||
, false, taxInfo, false, 8, 10);
|
|
||||||
|
|
||||||
Assert.Null(result);
|
|
||||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
|
||||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
|
||||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
|
||||||
Assert.True(organization.Enabled);
|
|
||||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
|
||||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
|
||||||
c.Description == organization.BusinessName &&
|
|
||||||
c.Email == organization.BillingEmail &&
|
|
||||||
c.Source == paymentToken &&
|
|
||||||
c.PaymentMethod == null &&
|
|
||||||
c.Metadata.Count == 1 &&
|
|
||||||
c.Metadata["region"] == "US" &&
|
|
||||||
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
|
||||||
c.InvoiceSettings.CustomFields != null &&
|
|
||||||
c.InvoiceSettings.CustomFields[0].Name == "Organization" &&
|
|
||||||
c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) &&
|
|
||||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
|
||||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
|
||||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
|
||||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
|
||||||
c.Address.City == taxInfo.BillingAddressCity &&
|
|
||||||
c.Address.State == taxInfo.BillingAddressState &&
|
|
||||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
|
||||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
|
||||||
));
|
|
||||||
|
|
||||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
|
||||||
s.Customer == "C-1" &&
|
|
||||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
|
||||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
|
||||||
s.Items.Count == 2
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_Stripe_PM(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
paymentToken = "pm_" + paymentToken;
|
|
||||||
|
|
||||||
sutProvider
|
|
||||||
.GetDependency<ITaxService>()
|
|
||||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
});
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>()
|
|
||||||
.BaseServiceUri.CloudRegion
|
|
||||||
.Returns("US");
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo);
|
|
||||||
|
|
||||||
Assert.Null(result);
|
|
||||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
|
||||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
|
||||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
|
||||||
Assert.True(organization.Enabled);
|
|
||||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
|
||||||
|
|
||||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
|
||||||
c.Description == organization.BusinessName &&
|
|
||||||
c.Email == organization.BillingEmail &&
|
|
||||||
c.Source == null &&
|
|
||||||
c.PaymentMethod == paymentToken &&
|
|
||||||
c.Metadata.Count == 1 &&
|
|
||||||
c.Metadata["region"] == "US" &&
|
|
||||||
c.InvoiceSettings.DefaultPaymentMethod == paymentToken &&
|
|
||||||
c.InvoiceSettings.CustomFields != null &&
|
|
||||||
c.InvoiceSettings.CustomFields[0].Name == "Organization" &&
|
|
||||||
c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) &&
|
|
||||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
|
||||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
|
||||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
|
||||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
|
||||||
c.Address.City == taxInfo.BillingAddressCity &&
|
|
||||||
c.Address.State == taxInfo.BillingAddressState &&
|
|
||||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
|
||||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
|
||||||
));
|
|
||||||
|
|
||||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
|
||||||
s.Customer == "C-1" &&
|
|
||||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
|
||||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
|
||||||
s.Items.Count == 0
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_Stripe_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
paymentToken = "pm_" + paymentToken;
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
Status = "incomplete",
|
|
||||||
LatestInvoice = new Stripe.Invoice
|
|
||||||
{
|
|
||||||
PaymentIntent = new Stripe.PaymentIntent
|
|
||||||
{
|
|
||||||
Status = "requires_payment_method",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
|
||||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo));
|
|
||||||
|
|
||||||
Assert.Equal("Payment method was declined.", exception.Message);
|
|
||||||
|
|
||||||
await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_SM_Stripe_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
paymentToken = "pm_" + paymentToken;
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
Status = "incomplete",
|
|
||||||
LatestInvoice = new Stripe.Invoice
|
|
||||||
{
|
|
||||||
PaymentIntent = new Stripe.PaymentIntent
|
|
||||||
{
|
|
||||||
Status = "requires_payment_method",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
|
||||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan,
|
|
||||||
1, 12, false, taxInfo, false, 10, 10));
|
|
||||||
|
|
||||||
Assert.Equal("Payment method was declined.", exception.Message);
|
|
||||||
|
|
||||||
await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_Stripe_RequiresAction(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
Status = "incomplete",
|
|
||||||
LatestInvoice = new Stripe.Invoice
|
|
||||||
{
|
|
||||||
PaymentIntent = new Stripe.PaymentIntent
|
|
||||||
{
|
|
||||||
Status = "requires_action",
|
|
||||||
ClientSecret = "clientSecret",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo);
|
|
||||||
|
|
||||||
Assert.Equal("clientSecret", result);
|
|
||||||
Assert.False(organization.Enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_SM_Stripe_RequiresAction(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
Status = "incomplete",
|
|
||||||
LatestInvoice = new Stripe.Invoice
|
|
||||||
{
|
|
||||||
PaymentIntent = new Stripe.PaymentIntent
|
|
||||||
{
|
|
||||||
Status = "requires_action",
|
|
||||||
ClientSecret = "clientSecret",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan,
|
|
||||||
10, 10, false, taxInfo, false, 10, 10);
|
|
||||||
|
|
||||||
Assert.Equal("clientSecret", result);
|
|
||||||
Assert.False(organization.Enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_Paypal(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
|
|
||||||
sutProvider
|
|
||||||
.GetDependency<ITaxService>()
|
|
||||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
});
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>()
|
|
||||||
.BaseServiceUri.CloudRegion
|
|
||||||
.Returns("US");
|
|
||||||
|
|
||||||
var customer = Substitute.For<Customer>();
|
|
||||||
customer.Id.ReturnsForAnyArgs("Braintree-Id");
|
|
||||||
customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For<PaymentMethod>() });
|
|
||||||
var customerResult = Substitute.For<Result<Customer>>();
|
|
||||||
customerResult.IsSuccess().Returns(true);
|
|
||||||
customerResult.Target.ReturnsForAnyArgs(customer);
|
|
||||||
|
|
||||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
|
||||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo);
|
|
||||||
|
|
||||||
Assert.Null(result);
|
|
||||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
|
||||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
|
||||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
|
||||||
Assert.True(organization.Enabled);
|
|
||||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
|
||||||
|
|
||||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
|
||||||
c.Description == organization.BusinessName &&
|
|
||||||
c.Email == organization.BillingEmail &&
|
|
||||||
c.PaymentMethod == null &&
|
|
||||||
c.Metadata.Count == 2 &&
|
|
||||||
c.Metadata["btCustomerId"] == "Braintree-Id" &&
|
|
||||||
c.Metadata["region"] == "US" &&
|
|
||||||
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
|
||||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
|
||||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
|
||||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
|
||||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
|
||||||
c.Address.City == taxInfo.BillingAddressCity &&
|
|
||||||
c.Address.State == taxInfo.BillingAddressState &&
|
|
||||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
|
||||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
|
||||||
));
|
|
||||||
|
|
||||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
|
||||||
s.Customer == "C-1" &&
|
|
||||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
|
||||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
|
||||||
s.Items.Count == 0
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_SM_Paypal(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
organization.UseSecretsManager = true;
|
|
||||||
|
|
||||||
sutProvider
|
|
||||||
.GetDependency<ITaxService>()
|
|
||||||
.GetStripeTaxCode(Arg.Is<string>(p => p == taxInfo.BillingAddressCountry), Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
|
||||||
.Returns(taxInfo.TaxIdType);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
});
|
|
||||||
|
|
||||||
var customer = Substitute.For<Customer>();
|
|
||||||
customer.Id.ReturnsForAnyArgs("Braintree-Id");
|
|
||||||
customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For<PaymentMethod>() });
|
|
||||||
var customerResult = Substitute.For<Result<Customer>>();
|
|
||||||
customerResult.IsSuccess().Returns(true);
|
|
||||||
customerResult.Target.ReturnsForAnyArgs(customer);
|
|
||||||
|
|
||||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
|
||||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>()
|
|
||||||
.BaseServiceUri.CloudRegion
|
|
||||||
.Returns("US");
|
|
||||||
|
|
||||||
var additionalStorage = (short)2;
|
|
||||||
var additionalSeats = 10;
|
|
||||||
var additionalSmSeats = 5;
|
|
||||||
var additionalServiceAccounts = 20;
|
|
||||||
var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan,
|
|
||||||
additionalStorage, additionalSeats, false, taxInfo, false, additionalSmSeats, additionalServiceAccounts);
|
|
||||||
|
|
||||||
Assert.Null(result);
|
|
||||||
Assert.Equal(GatewayType.Stripe, organization.Gateway);
|
|
||||||
Assert.Equal("C-1", organization.GatewayCustomerId);
|
|
||||||
Assert.Equal("S-1", organization.GatewaySubscriptionId);
|
|
||||||
Assert.True(organization.Enabled);
|
|
||||||
Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
|
|
||||||
|
|
||||||
await stripeAdapter.Received().CustomerCreateAsync(Arg.Is<Stripe.CustomerCreateOptions>(c =>
|
|
||||||
c.Description == organization.BusinessName &&
|
|
||||||
c.Email == organization.BillingEmail &&
|
|
||||||
c.PaymentMethod == null &&
|
|
||||||
c.Metadata.Count == 2 &&
|
|
||||||
c.Metadata["region"] == "US" &&
|
|
||||||
c.Metadata["btCustomerId"] == "Braintree-Id" &&
|
|
||||||
c.InvoiceSettings.DefaultPaymentMethod == null &&
|
|
||||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
|
||||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
|
||||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
|
||||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
|
||||||
c.Address.City == taxInfo.BillingAddressCity &&
|
|
||||||
c.Address.State == taxInfo.BillingAddressState &&
|
|
||||||
c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
|
|
||||||
c.TaxIdData.First().Type == taxInfo.TaxIdType
|
|
||||||
));
|
|
||||||
|
|
||||||
await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is<Stripe.SubscriptionCreateOptions>(s =>
|
|
||||||
s.Customer == "C-1" &&
|
|
||||||
s.Expand[0] == "latest_invoice.payment_intent" &&
|
|
||||||
s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
|
|
||||||
s.Items.Count == 4 &&
|
|
||||||
s.Items.Count(i => i.Plan == plan.PasswordManager.StripeSeatPlanId && i.Quantity == additionalSeats) == 1 &&
|
|
||||||
s.Items.Count(i => i.Plan == plan.PasswordManager.StripeStoragePlanId && i.Quantity == additionalStorage) == 1 &&
|
|
||||||
s.Items.Count(i => i.Plan == plan.SecretsManager.StripeSeatPlanId && i.Quantity == additionalSmSeats) == 1 &&
|
|
||||||
s.Items.Count(i => i.Plan == plan.SecretsManager.StripeServiceAccountPlanId && i.Quantity == additionalServiceAccounts) == 1
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_Paypal_FailedCreate(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
|
|
||||||
var customerResult = Substitute.For<Result<Customer>>();
|
|
||||||
customerResult.IsSuccess().Returns(false);
|
|
||||||
|
|
||||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
|
||||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
|
||||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo));
|
|
||||||
|
|
||||||
Assert.Equal("Failed to create PayPal customer record.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_SM_Paypal_FailedCreate(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
|
|
||||||
var customerResult = Substitute.For<Result<Customer>>();
|
|
||||||
customerResult.IsSuccess().Returns(false);
|
|
||||||
|
|
||||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
|
||||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
|
||||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan,
|
|
||||||
1, 1, false, taxInfo, false, 8, 8));
|
|
||||||
|
|
||||||
Assert.Equal("Failed to create PayPal customer record.", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task PurchaseOrganizationAsync_PayPal_Declined(SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
var plans = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
paymentToken = "pm_" + paymentToken;
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
Status = "incomplete",
|
|
||||||
LatestInvoice = new Stripe.Invoice
|
|
||||||
{
|
|
||||||
PaymentIntent = new Stripe.PaymentIntent
|
|
||||||
{
|
|
||||||
Status = "requires_payment_method",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var customer = Substitute.For<Customer>();
|
|
||||||
customer.Id.ReturnsForAnyArgs("Braintree-Id");
|
|
||||||
customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For<PaymentMethod>() });
|
|
||||||
var customerResult = Substitute.For<Result<Customer>>();
|
|
||||||
customerResult.IsSuccess().Returns(true);
|
|
||||||
customerResult.Target.ReturnsForAnyArgs(customer);
|
|
||||||
|
|
||||||
var braintreeGateway = sutProvider.GetDependency<IBraintreeGateway>();
|
|
||||||
braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<GatewayException>(
|
|
||||||
() => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plans, 0, 0, false, taxInfo));
|
|
||||||
|
|
||||||
Assert.Equal("Payment method was declined.", exception.Message);
|
|
||||||
|
|
||||||
await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
|
|
||||||
await braintreeGateway.Customer.Received(1).DeleteAsync("Braintree-Id");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData("ES", "A5372895732985327895237")]
|
|
||||||
public async Task PurchaseOrganizationAsync_ThrowsBadRequestException_WhenTaxIdInvalid(string country, string taxId, SutProvider<StripePaymentService> sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
taxInfo.BillingAddressCountry = country;
|
|
||||||
taxInfo.TaxIdNumber = taxId;
|
|
||||||
taxInfo.TaxIdType = null;
|
|
||||||
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
organization.UseSecretsManager = true;
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
|
|
||||||
{
|
|
||||||
Id = "S-1",
|
|
||||||
CurrentPeriodEnd = DateTime.Today.AddDays(10),
|
|
||||||
});
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>()
|
|
||||||
.BaseServiceUri.CloudRegion
|
|
||||||
.Returns("US");
|
|
||||||
sutProvider
|
|
||||||
.GetDependency<ITaxService>()
|
|
||||||
.GetStripeTaxCode(Arg.Is<string>(p => p == country), Arg.Is<string>(p => p == taxId))
|
|
||||||
.Returns((string)null);
|
|
||||||
|
|
||||||
var actual = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo, false, 8, 10));
|
|
||||||
|
|
||||||
Assert.Equal("billingTaxIdTypeInferenceError", actual.Message);
|
|
||||||
|
|
||||||
await stripeAdapter.Received(0).CustomerCreateAsync(Arg.Any<Stripe.CustomerCreateOptions>());
|
|
||||||
await stripeAdapter.Received(0).SubscriptionCreateAsync(Arg.Any<Stripe.SubscriptionCreateOptions>());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task UpgradeFreeOrganizationAsync_Success(SutProvider<StripePaymentService> sutProvider,
|
|
||||||
Organization organization, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
organization.GatewaySubscriptionId = null;
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
Metadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "btCustomerId", "B-123" },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
Metadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "btCustomerId", "B-123" },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice
|
|
||||||
{
|
|
||||||
PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", },
|
|
||||||
AmountDue = 0
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
|
|
||||||
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
|
|
||||||
var upgrade = new OrganizationUpgrade()
|
|
||||||
{
|
|
||||||
AdditionalStorageGb = 0,
|
|
||||||
AdditionalSeats = 0,
|
|
||||||
PremiumAccessAddon = false,
|
|
||||||
TaxInfo = taxInfo,
|
|
||||||
AdditionalSmSeats = 0,
|
|
||||||
AdditionalServiceAccounts = 0
|
|
||||||
};
|
|
||||||
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade);
|
|
||||||
|
|
||||||
Assert.Null(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task UpgradeFreeOrganizationAsync_SM_Success(SutProvider<StripePaymentService> sutProvider,
|
|
||||||
Organization organization, TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
organization.GatewaySubscriptionId = null;
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
Metadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "btCustomerId", "B-123" },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
Metadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "btCustomerId", "B-123" },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice
|
|
||||||
{
|
|
||||||
PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", },
|
|
||||||
AmountDue = 0
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
|
|
||||||
|
|
||||||
var upgrade = new OrganizationUpgrade()
|
|
||||||
{
|
|
||||||
AdditionalStorageGb = 1,
|
|
||||||
AdditionalSeats = 10,
|
|
||||||
PremiumAccessAddon = false,
|
|
||||||
TaxInfo = taxInfo,
|
|
||||||
AdditionalSmSeats = 5,
|
|
||||||
AdditionalServiceAccounts = 50
|
|
||||||
};
|
|
||||||
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade);
|
|
||||||
|
|
||||||
Assert.Null(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task UpgradeFreeOrganizationAsync_WhenCustomerHasNoAddress_UpdatesCustomerAddressWithTaxInfo(
|
|
||||||
SutProvider<StripePaymentService> sutProvider,
|
|
||||||
Organization organization,
|
|
||||||
TaxInfo taxInfo)
|
|
||||||
{
|
|
||||||
organization.GatewaySubscriptionId = null;
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
var featureService = sutProvider.GetDependency<IFeatureService>();
|
|
||||||
stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
Metadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "btCustomerId", "B-123" },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
|
|
||||||
{
|
|
||||||
Id = "C-1",
|
|
||||||
Metadata = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "btCustomerId", "B-123" },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice
|
|
||||||
{
|
|
||||||
PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", },
|
|
||||||
AmountDue = 0
|
|
||||||
});
|
|
||||||
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
|
|
||||||
|
|
||||||
var upgrade = new OrganizationUpgrade()
|
|
||||||
{
|
|
||||||
AdditionalStorageGb = 1,
|
|
||||||
AdditionalSeats = 10,
|
|
||||||
PremiumAccessAddon = false,
|
|
||||||
TaxInfo = taxInfo,
|
|
||||||
AdditionalSmSeats = 5,
|
|
||||||
AdditionalServiceAccounts = 50
|
|
||||||
};
|
|
||||||
|
|
||||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
|
||||||
_ = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade);
|
|
||||||
|
|
||||||
await stripeAdapter.Received()
|
|
||||||
.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<Stripe.CustomerUpdateOptions>(c =>
|
|
||||||
c.Address.Country == taxInfo.BillingAddressCountry &&
|
|
||||||
c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
|
|
||||||
c.Address.Line1 == taxInfo.BillingAddressLine1 &&
|
|
||||||
c.Address.Line2 == taxInfo.BillingAddressLine2 &&
|
|
||||||
c.Address.City == taxInfo.BillingAddressCity &&
|
|
||||||
c.Address.State == taxInfo.BillingAddressState));
|
|
||||||
}
|
|
||||||
}
|
|
@ -44,7 +44,7 @@ public class ImportCiphersAsyncCommandTests
|
|||||||
var folderRelationships = new List<KeyValuePair<int, int>>();
|
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships);
|
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
|
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
|
||||||
@ -68,7 +68,7 @@ public class ImportCiphersAsyncCommandTests
|
|||||||
var folderRelationships = new List<KeyValuePair<int, int>>();
|
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships));
|
sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, userId));
|
||||||
|
|
||||||
Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message);
|
Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadByClaimedUserEmailDomain]
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
WITH CTE_User AS (
|
||||||
|
SELECT
|
||||||
|
U.*,
|
||||||
|
SUBSTRING(U.Email, CHARINDEX('@', U.Email) + 1, LEN(U.Email)) AS EmailDomain
|
||||||
|
FROM dbo.[UserView] U
|
||||||
|
WHERE U.[Id] = @UserId
|
||||||
|
)
|
||||||
|
SELECT O.*
|
||||||
|
FROM CTE_User CU
|
||||||
|
INNER JOIN dbo.[OrganizationUserView] OU ON CU.[Id] = OU.[UserId]
|
||||||
|
INNER JOIN dbo.[OrganizationView] O ON OU.[OrganizationId] = O.[Id]
|
||||||
|
INNER JOIN dbo.[OrganizationDomainView] OD ON OU.[OrganizationId] = OD.[OrganizationId]
|
||||||
|
WHERE OD.[VerifiedDate] IS NOT NULL
|
||||||
|
AND CU.EmailDomain = OD.[DomainName]
|
||||||
|
AND O.[Enabled] = 1
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationDomain_DomainNameVerifiedDateOrganizationId')
|
||||||
|
BEGIN
|
||||||
|
CREATE NONCLUSTERED INDEX [IX_OrganizationDomain_DomainNameVerifiedDateOrganizationId]
|
||||||
|
ON [dbo].[OrganizationDomain] ([DomainName],[VerifiedDate])
|
||||||
|
INCLUDE ([OrganizationId])
|
||||||
|
END
|
||||||
|
GO
|
Loading…
x
Reference in New Issue
Block a user