mirror of
https://github.com/bitwarden/server
synced 2025-12-19 17:53:44 +00:00
[PM-24964] Stripe-hosted bank account verification (#6263)
* Implement bank account hosted URL verification with webhook handling notification * Fix tests * Run dotnet format * Remove unused VerifyBankAccount operation * Stephon's feedback * Removing unused test * TEMP: Add logging for deployment check * Run dotnet format * fix test * Revert "fix test" This reverts commitb8743ab3b5. * Revert "Run dotnet format" This reverts commit5c861b0b72. * Revert "TEMP: Add logging for deployment check" This reverts commit0a88acd6a1. * Resolve GetPaymentMethodQuery order of operations
This commit is contained in:
@@ -636,10 +636,10 @@ public class ProviderBillingService(
|
|||||||
{
|
{
|
||||||
case PaymentMethodType.BankAccount:
|
case PaymentMethodType.BankAccount:
|
||||||
{
|
{
|
||||||
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
|
||||||
await stripeAdapter.SetupIntentCancel(setupIntentId,
|
await stripeAdapter.SetupIntentCancel(setupIntentId,
|
||||||
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
|
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
|
||||||
await setupIntentCache.Remove(provider.Id);
|
await setupIntentCache.RemoveSetupIntentForSubscriber(provider.Id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||||
@@ -689,7 +689,7 @@ public class ProviderBillingService(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
|
||||||
|
|
||||||
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
|
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
|
||||||
? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
|
? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
|
||||||
|
|||||||
@@ -1003,7 +1003,7 @@ public class ProviderBillingServiceTests
|
|||||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
||||||
.Throws<StripeException>();
|
.Throws<StripeException>();
|
||||||
|
|
||||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns("setup_intent_id");
|
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns("setup_intent_id");
|
||||||
|
|
||||||
await Assert.ThrowsAsync<StripeException>(() =>
|
await Assert.ThrowsAsync<StripeException>(() =>
|
||||||
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||||
@@ -1013,7 +1013,7 @@ public class ProviderBillingServiceTests
|
|||||||
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
|
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
|
||||||
options.CancellationReason == "abandoned"));
|
options.CancellationReason == "abandoned"));
|
||||||
|
|
||||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Remove(provider.Id);
|
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).RemoveSetupIntentForSubscriber(provider.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@@ -1644,7 +1644,7 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
const string setupIntentId = "seti_123";
|
const string setupIntentId = "seti_123";
|
||||||
|
|
||||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntentId);
|
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntentId);
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
|
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
|
||||||
options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ public class OrganizationBillingVNextController(
|
|||||||
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
|
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
|
||||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||||
IUpdateBillingAddressCommand updateBillingAddressCommand,
|
IUpdateBillingAddressCommand updateBillingAddressCommand,
|
||||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
|
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
|
||||||
IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController
|
|
||||||
{
|
{
|
||||||
[Authorize<ManageOrganizationBillingRequirement>]
|
[Authorize<ManageOrganizationBillingRequirement>]
|
||||||
[HttpGet("address")]
|
[HttpGet("address")]
|
||||||
@@ -96,17 +95,6 @@ public class OrganizationBillingVNextController(
|
|||||||
return Handle(result);
|
return Handle(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize<ManageOrganizationBillingRequirement>]
|
|
||||||
[HttpPost("payment-method/verify-bank-account")]
|
|
||||||
[InjectOrganization]
|
|
||||||
public async Task<IResult> VerifyBankAccountAsync(
|
|
||||||
[BindNever] Organization organization,
|
|
||||||
[FromBody] VerifyBankAccountRequest request)
|
|
||||||
{
|
|
||||||
var result = await verifyBankAccountCommand.Run(organization, request.DescriptorCode);
|
|
||||||
return Handle(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize<MemberOrProviderRequirement>]
|
[Authorize<MemberOrProviderRequirement>]
|
||||||
[HttpGet("warnings")]
|
[HttpGet("warnings")]
|
||||||
[InjectOrganization]
|
[InjectOrganization]
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ public class ProviderBillingVNextController(
|
|||||||
IGetProviderWarningsQuery getProviderWarningsQuery,
|
IGetProviderWarningsQuery getProviderWarningsQuery,
|
||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
IUpdateBillingAddressCommand updateBillingAddressCommand,
|
IUpdateBillingAddressCommand updateBillingAddressCommand,
|
||||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
|
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
|
||||||
IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController
|
|
||||||
{
|
{
|
||||||
[HttpGet("address")]
|
[HttpGet("address")]
|
||||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
||||||
@@ -97,16 +96,6 @@ public class ProviderBillingVNextController(
|
|||||||
return Handle(result);
|
return Handle(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("payment-method/verify-bank-account")]
|
|
||||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
|
||||||
public async Task<IResult> VerifyBankAccountAsync(
|
|
||||||
[BindNever] Provider provider,
|
|
||||||
[FromBody] VerifyBankAccountRequest request)
|
|
||||||
{
|
|
||||||
var result = await verifyBankAccountCommand.Run(provider, request.DescriptorCode);
|
|
||||||
return Handle(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("warnings")]
|
[HttpGet("warnings")]
|
||||||
[InjectProvider(ProviderUserType.ServiceUser)]
|
[InjectProvider(ProviderUserType.ServiceUser)]
|
||||||
public async Task<IResult> GetWarningsAsync(
|
public async Task<IResult> GetWarningsAsync(
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ public static class HandledStripeWebhook
|
|||||||
public const string PaymentMethodAttached = "payment_method.attached";
|
public const string PaymentMethodAttached = "payment_method.attached";
|
||||||
public const string CustomerUpdated = "customer.updated";
|
public const string CustomerUpdated = "customer.updated";
|
||||||
public const string InvoiceFinalized = "invoice.finalized";
|
public const string InvoiceFinalized = "invoice.finalized";
|
||||||
|
public const string SetupIntentSucceeded = "setup_intent.succeeded";
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/Billing/Services/IPushNotificationAdapter.cs
Normal file
11
src/Billing/Services/IPushNotificationAdapter.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
|
||||||
|
namespace Bit.Billing.Services;
|
||||||
|
|
||||||
|
public interface IPushNotificationAdapter
|
||||||
|
{
|
||||||
|
Task NotifyBankAccountVerifiedAsync(Organization organization);
|
||||||
|
Task NotifyBankAccountVerifiedAsync(Provider provider);
|
||||||
|
Task NotifyEnabledChangedAsync(Organization organization);
|
||||||
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
using Stripe;
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using Stripe;
|
|
||||||
|
|
||||||
namespace Bit.Billing.Services;
|
namespace Bit.Billing.Services;
|
||||||
|
|
||||||
@@ -13,12 +10,10 @@ public interface IStripeEventService
|
|||||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||||
/// <param name="fresh">Determines whether or not to retrieve a fresh copy of the charge object from Stripe.</param>
|
/// <param name="fresh">Determines whether to retrieve a fresh copy of the charge object from Stripe.</param>
|
||||||
/// <param name="expand">Optionally provided to expand the fresh charge object retrieved from Stripe.</param>
|
/// <param name="expand">Optionally provided to expand the fresh charge object retrieved from Stripe.</param>
|
||||||
/// <returns>A Stripe <see cref="Charge"/>.</returns>
|
/// <returns>A Stripe <see cref="Charge"/>.</returns>
|
||||||
/// <exception cref="Exception">Thrown when the Stripe event does not contain a charge object.</exception>
|
Task<Charge> GetCharge(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||||
/// <exception cref="Exception">Thrown when <paramref name="fresh"/> is true and Stripe's API returns a null charge object.</exception>
|
|
||||||
Task<Charge> GetCharge(Event stripeEvent, bool fresh = false, List<string> expand = null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts the <see cref="Customer"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
/// Extracts the <see cref="Customer"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
||||||
@@ -26,12 +21,10 @@ public interface IStripeEventService
|
|||||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||||
/// <param name="fresh">Determines whether or not to retrieve a fresh copy of the customer object from Stripe.</param>
|
/// <param name="fresh">Determines whether to retrieve a fresh copy of the customer object from Stripe.</param>
|
||||||
/// <param name="expand">Optionally provided to expand the fresh customer object retrieved from Stripe.</param>
|
/// <param name="expand">Optionally provided to expand the fresh customer object retrieved from Stripe.</param>
|
||||||
/// <returns>A Stripe <see cref="Customer"/>.</returns>
|
/// <returns>A Stripe <see cref="Customer"/>.</returns>
|
||||||
/// <exception cref="Exception">Thrown when the Stripe event does not contain a customer object.</exception>
|
Task<Customer> GetCustomer(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||||
/// <exception cref="Exception">Thrown when <paramref name="fresh"/> is true and Stripe's API returns a null customer object.</exception>
|
|
||||||
Task<Customer> GetCustomer(Event stripeEvent, bool fresh = false, List<string> expand = null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts the <see cref="Invoice"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
/// Extracts the <see cref="Invoice"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
||||||
@@ -39,12 +32,10 @@ public interface IStripeEventService
|
|||||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||||
/// <param name="fresh">Determines whether or not to retrieve a fresh copy of the invoice object from Stripe.</param>
|
/// <param name="fresh">Determines whether to retrieve a fresh copy of the invoice object from Stripe.</param>
|
||||||
/// <param name="expand">Optionally provided to expand the fresh invoice object retrieved from Stripe.</param>
|
/// <param name="expand">Optionally provided to expand the fresh invoice object retrieved from Stripe.</param>
|
||||||
/// <returns>A Stripe <see cref="Invoice"/>.</returns>
|
/// <returns>A Stripe <see cref="Invoice"/>.</returns>
|
||||||
/// <exception cref="Exception">Thrown when the Stripe event does not contain an invoice object.</exception>
|
Task<Invoice> GetInvoice(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||||
/// <exception cref="Exception">Thrown when <paramref name="fresh"/> is true and Stripe's API returns a null invoice object.</exception>
|
|
||||||
Task<Invoice> GetInvoice(Event stripeEvent, bool fresh = false, List<string> expand = null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts the <see cref="PaymentMethod"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
/// Extracts the <see cref="PaymentMethod"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
||||||
@@ -52,12 +43,21 @@ public interface IStripeEventService
|
|||||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||||
/// <param name="fresh">Determines whether or not to retrieve a fresh copy of the payment method object from Stripe.</param>
|
/// <param name="fresh">Determines whether to retrieve a fresh copy of the payment method object from Stripe.</param>
|
||||||
/// <param name="expand">Optionally provided to expand the fresh payment method object retrieved from Stripe.</param>
|
/// <param name="expand">Optionally provided to expand the fresh payment method object retrieved from Stripe.</param>
|
||||||
/// <returns>A Stripe <see cref="PaymentMethod"/>.</returns>
|
/// <returns>A Stripe <see cref="PaymentMethod"/>.</returns>
|
||||||
/// <exception cref="Exception">Thrown when the Stripe event does not contain an payment method object.</exception>
|
Task<PaymentMethod> GetPaymentMethod(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||||
/// <exception cref="Exception">Thrown when <paramref name="fresh"/> is true and Stripe's API returns a null payment method object.</exception>
|
|
||||||
Task<PaymentMethod> GetPaymentMethod(Event stripeEvent, bool fresh = false, List<string> expand = null);
|
/// <summary>
|
||||||
|
/// Extracts the <see cref="SetupIntent"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
||||||
|
/// uses the setup intent ID extracted from the event to retrieve the most up-to-update setup intent from Stripe's API
|
||||||
|
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||||
|
/// <param name="fresh">Determines whether to retrieve a fresh copy of the setup intent object from Stripe.</param>
|
||||||
|
/// <param name="expand">Optionally provided to expand the fresh setup intent object retrieved from Stripe.</param>
|
||||||
|
/// <returns>A Stripe <see cref="SetupIntent"/>.</returns>
|
||||||
|
Task<SetupIntent> GetSetupIntent(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts the <see cref="Subscription"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
/// Extracts the <see cref="Subscription"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
||||||
@@ -65,12 +65,10 @@ public interface IStripeEventService
|
|||||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||||
/// <param name="fresh">Determines whether or not to retrieve a fresh copy of the subscription object from Stripe.</param>
|
/// <param name="fresh">Determines whether to retrieve a fresh copy of the subscription object from Stripe.</param>
|
||||||
/// <param name="expand">Optionally provided to expand the fresh subscription object retrieved from Stripe.</param>
|
/// <param name="expand">Optionally provided to expand the fresh subscription object retrieved from Stripe.</param>
|
||||||
/// <returns>A Stripe <see cref="Subscription"/>.</returns>
|
/// <returns>A Stripe <see cref="Subscription"/>.</returns>
|
||||||
/// <exception cref="Exception">Thrown when the Stripe event does not contain an subscription object.</exception>
|
Task<Subscription> GetSubscription(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||||
/// <exception cref="Exception">Thrown when <paramref name="fresh"/> is true and Stripe's API returns a null subscription object.</exception>
|
|
||||||
Task<Subscription> GetSubscription(Event stripeEvent, bool fresh = false, List<string> expand = null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ensures that the customer associated with the Stripe <see cref="Event"/> is in the correct region for this server.
|
/// Ensures that the customer associated with the Stripe <see cref="Event"/> is in the correct region for this server.
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ public interface IStripeFacade
|
|||||||
RequestOptions requestOptions = null,
|
RequestOptions requestOptions = null,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<SetupIntent> GetSetupIntent(
|
||||||
|
string setupIntentId,
|
||||||
|
SetupIntentGetOptions setupIntentGetOptions = null,
|
||||||
|
RequestOptions requestOptions = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<StripeList<Invoice>> ListInvoices(
|
Task<StripeList<Invoice>> ListInvoices(
|
||||||
InvoiceListOptions options = null,
|
InvoiceListOptions options = null,
|
||||||
RequestOptions requestOptions = null,
|
RequestOptions requestOptions = null,
|
||||||
|
|||||||
@@ -65,3 +65,5 @@ public interface ICustomerUpdatedHandler : IStripeWebhookHandler;
|
|||||||
/// Defines the contract for handling Stripe Invoice Finalized events.
|
/// Defines the contract for handling Stripe Invoice Finalized events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IInvoiceFinalizedHandler : IStripeWebhookHandler;
|
public interface IInvoiceFinalizedHandler : IStripeWebhookHandler;
|
||||||
|
|
||||||
|
public interface ISetupIntentSucceededHandler : IStripeWebhookHandler;
|
||||||
|
|||||||
@@ -3,27 +3,13 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Platform.Push;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Event = Stripe.Event;
|
using Event = Stripe.Event;
|
||||||
|
|
||||||
namespace Bit.Billing.Services.Implementations;
|
namespace Bit.Billing.Services.Implementations;
|
||||||
|
|
||||||
public class PaymentSucceededHandler : IPaymentSucceededHandler
|
public class PaymentSucceededHandler(
|
||||||
{
|
|
||||||
private readonly ILogger<PaymentSucceededHandler> _logger;
|
|
||||||
private readonly IStripeEventService _stripeEventService;
|
|
||||||
private readonly IUserService _userService;
|
|
||||||
private readonly IStripeFacade _stripeFacade;
|
|
||||||
private readonly IProviderRepository _providerRepository;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
|
||||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
|
||||||
private readonly IOrganizationEnableCommand _organizationEnableCommand;
|
|
||||||
private readonly IPricingClient _pricingClient;
|
|
||||||
|
|
||||||
public PaymentSucceededHandler(
|
|
||||||
ILogger<PaymentSucceededHandler> logger,
|
ILogger<PaymentSucceededHandler> logger,
|
||||||
IStripeEventService stripeEventService,
|
IStripeEventService stripeEventService,
|
||||||
IStripeFacade stripeFacade,
|
IStripeFacade stripeFacade,
|
||||||
@@ -31,35 +17,24 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IStripeEventUtilityService stripeEventUtilityService,
|
IStripeEventUtilityService stripeEventUtilityService,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IPushNotificationService pushNotificationService,
|
|
||||||
IOrganizationEnableCommand organizationEnableCommand,
|
IOrganizationEnableCommand organizationEnableCommand,
|
||||||
IPricingClient pricingClient)
|
IPricingClient pricingClient,
|
||||||
{
|
IPushNotificationAdapter pushNotificationAdapter)
|
||||||
_logger = logger;
|
: IPaymentSucceededHandler
|
||||||
_stripeEventService = stripeEventService;
|
{
|
||||||
_stripeFacade = stripeFacade;
|
|
||||||
_providerRepository = providerRepository;
|
|
||||||
_organizationRepository = organizationRepository;
|
|
||||||
_stripeEventUtilityService = stripeEventUtilityService;
|
|
||||||
_userService = userService;
|
|
||||||
_pushNotificationService = pushNotificationService;
|
|
||||||
_organizationEnableCommand = organizationEnableCommand;
|
|
||||||
_pricingClient = pricingClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the <see cref="HandledStripeWebhook.PaymentSucceeded"/> event type from Stripe.
|
/// Handles the <see cref="HandledStripeWebhook.PaymentSucceeded"/> event type from Stripe.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parsedEvent"></param>
|
/// <param name="parsedEvent"></param>
|
||||||
public async Task HandleAsync(Event parsedEvent)
|
public async Task HandleAsync(Event parsedEvent)
|
||||||
{
|
{
|
||||||
var invoice = await _stripeEventService.GetInvoice(parsedEvent, true);
|
var invoice = await stripeEventService.GetInvoice(parsedEvent, true);
|
||||||
if (!invoice.Paid || invoice.BillingReason != "subscription_create")
|
if (!invoice.Paid || invoice.BillingReason != "subscription_create")
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId);
|
var subscription = await stripeFacade.GetSubscription(invoice.SubscriptionId);
|
||||||
if (subscription?.Status != StripeSubscriptionStatus.Active)
|
if (subscription?.Status != StripeSubscriptionStatus.Active)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -70,15 +45,15 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
await Task.Delay(5000);
|
await Task.Delay(5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
||||||
|
|
||||||
if (providerId.HasValue)
|
if (providerId.HasValue)
|
||||||
{
|
{
|
||||||
var provider = await _providerRepository.GetByIdAsync(providerId.Value);
|
var provider = await providerRepository.GetByIdAsync(providerId.Value);
|
||||||
|
|
||||||
if (provider == null)
|
if (provider == null)
|
||||||
{
|
{
|
||||||
_logger.LogError(
|
logger.LogError(
|
||||||
"Received invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) that does not exist",
|
"Received invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) that does not exist",
|
||||||
parsedEvent.Id,
|
parsedEvent.Id,
|
||||||
providerId.Value);
|
providerId.Value);
|
||||||
@@ -86,9 +61,9 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var teamsMonthly = await _pricingClient.GetPlanOrThrow(PlanType.TeamsMonthly);
|
var teamsMonthly = await pricingClient.GetPlanOrThrow(PlanType.TeamsMonthly);
|
||||||
|
|
||||||
var enterpriseMonthly = await _pricingClient.GetPlanOrThrow(PlanType.EnterpriseMonthly);
|
var enterpriseMonthly = await pricingClient.GetPlanOrThrow(PlanType.EnterpriseMonthly);
|
||||||
|
|
||||||
var teamsMonthlyLineItem =
|
var teamsMonthlyLineItem =
|
||||||
subscription.Items.Data.FirstOrDefault(item =>
|
subscription.Items.Data.FirstOrDefault(item =>
|
||||||
@@ -100,29 +75,30 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
|
|
||||||
if (teamsMonthlyLineItem == null || enterpriseMonthlyLineItem == null)
|
if (teamsMonthlyLineItem == null || enterpriseMonthlyLineItem == null)
|
||||||
{
|
{
|
||||||
_logger.LogError("invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items",
|
logger.LogError("invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items",
|
||||||
parsedEvent.Id,
|
parsedEvent.Id,
|
||||||
provider.Id);
|
provider.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (organizationId.HasValue)
|
else if (organizationId.HasValue)
|
||||||
{
|
{
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
|
var organization = await organizationRepository.GetByIdAsync(organizationId.Value);
|
||||||
|
|
||||||
if (organization == null)
|
if (organization == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||||
|
|
||||||
if (subscription.Items.All(item => plan.PasswordManager.StripePlanId != item.Plan.Id))
|
if (subscription.Items.All(item => plan.PasswordManager.StripePlanId != item.Plan.Id))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
await organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
||||||
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);
|
organization = await organizationRepository.GetByIdAsync(organization.Id);
|
||||||
|
await pushNotificationAdapter.NotifyEnabledChangedAsync(organization!);
|
||||||
}
|
}
|
||||||
else if (userId.HasValue)
|
else if (userId.HasValue)
|
||||||
{
|
{
|
||||||
@@ -131,7 +107,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd);
|
await userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
|
|
||||||
|
namespace Bit.Billing.Services.Implementations;
|
||||||
|
|
||||||
|
public class PushNotificationAdapter(
|
||||||
|
IProviderUserRepository providerUserRepository,
|
||||||
|
IPushNotificationService pushNotificationService) : IPushNotificationAdapter
|
||||||
|
{
|
||||||
|
public Task NotifyBankAccountVerifiedAsync(Organization organization) =>
|
||||||
|
pushNotificationService.PushAsync(new PushNotification<OrganizationBankAccountVerifiedPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.OrganizationBankAccountVerified,
|
||||||
|
Target = NotificationTarget.Organization,
|
||||||
|
TargetId = organization.Id,
|
||||||
|
Payload = new OrganizationBankAccountVerifiedPushNotification
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false
|
||||||
|
});
|
||||||
|
|
||||||
|
public async Task NotifyBankAccountVerifiedAsync(Provider provider)
|
||||||
|
{
|
||||||
|
var providerUsers = await providerUserRepository.GetManyByProviderAsync(provider.Id);
|
||||||
|
var providerAdmins = providerUsers.Where(providerUser => providerUser is
|
||||||
|
{
|
||||||
|
Type: ProviderUserType.ProviderAdmin,
|
||||||
|
Status: ProviderUserStatusType.Confirmed,
|
||||||
|
UserId: not null
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
if (providerAdmins.Count > 0)
|
||||||
|
{
|
||||||
|
var tasks = providerAdmins.Select(providerAdmin => pushNotificationService.PushAsync(
|
||||||
|
new PushNotification<ProviderBankAccountVerifiedPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.ProviderBankAccountVerified,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = providerAdmin.UserId!.Value,
|
||||||
|
Payload = new ProviderBankAccountVerifiedPushNotification
|
||||||
|
{
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
AdminId = providerAdmin.UserId!.Value
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false
|
||||||
|
}));
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task NotifyEnabledChangedAsync(Organization organization) =>
|
||||||
|
pushNotificationService.PushAsync(new PushNotification<OrganizationStatusPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncOrganizationStatusChanged,
|
||||||
|
Target = NotificationTarget.Organization,
|
||||||
|
TargetId = organization.Id,
|
||||||
|
Payload = new OrganizationStatusPushNotification
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
Enabled = organization.Enabled,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Caches;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using OneOf;
|
||||||
|
using Stripe;
|
||||||
|
using Event = Stripe.Event;
|
||||||
|
|
||||||
|
namespace Bit.Billing.Services.Implementations;
|
||||||
|
|
||||||
|
public class SetupIntentSucceededHandler(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IProviderRepository providerRepository,
|
||||||
|
IPushNotificationAdapter pushNotificationAdapter,
|
||||||
|
ISetupIntentCache setupIntentCache,
|
||||||
|
IStripeAdapter stripeAdapter,
|
||||||
|
IStripeEventService stripeEventService) : ISetupIntentSucceededHandler
|
||||||
|
{
|
||||||
|
public async Task HandleAsync(Event parsedEvent)
|
||||||
|
{
|
||||||
|
var setupIntent = await stripeEventService.GetSetupIntent(
|
||||||
|
parsedEvent,
|
||||||
|
true,
|
||||||
|
["payment_method"]);
|
||||||
|
|
||||||
|
if (setupIntent is not
|
||||||
|
{
|
||||||
|
PaymentMethod.UsBankAccount: not null
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscriberId = await setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id);
|
||||||
|
if (subscriberId == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(subscriberId.Value);
|
||||||
|
var provider = await providerRepository.GetByIdAsync(subscriberId.Value);
|
||||||
|
|
||||||
|
OneOf<Organization, Provider> entity = organization != null ? organization : provider!;
|
||||||
|
await SetPaymentMethodAsync(entity, setupIntent.PaymentMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetPaymentMethodAsync(
|
||||||
|
OneOf<Organization, Provider> subscriber,
|
||||||
|
PaymentMethod paymentMethod)
|
||||||
|
{
|
||||||
|
var customerId = subscriber.Match(
|
||||||
|
organization => organization.GatewayCustomerId,
|
||||||
|
provider => provider.GatewayCustomerId);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(customerId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await stripeAdapter.PaymentMethodAttachAsync(paymentMethod.Id,
|
||||||
|
new PaymentMethodAttachOptions { Customer = customerId });
|
||||||
|
|
||||||
|
await stripeAdapter.CustomerUpdateAsync(customerId, new CustomerUpdateOptions
|
||||||
|
{
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||||
|
{
|
||||||
|
DefaultPaymentMethod = paymentMethod.Id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await subscriber.Match(
|
||||||
|
async organization => await pushNotificationAdapter.NotifyBankAccountVerifiedAsync(organization),
|
||||||
|
async provider => await pushNotificationAdapter.NotifyBankAccountVerifiedAsync(provider));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,22 +3,7 @@ using Event = Stripe.Event;
|
|||||||
|
|
||||||
namespace Bit.Billing.Services.Implementations;
|
namespace Bit.Billing.Services.Implementations;
|
||||||
|
|
||||||
public class StripeEventProcessor : IStripeEventProcessor
|
public class StripeEventProcessor(
|
||||||
{
|
|
||||||
private readonly ILogger<StripeEventProcessor> _logger;
|
|
||||||
private readonly ISubscriptionDeletedHandler _subscriptionDeletedHandler;
|
|
||||||
private readonly ISubscriptionUpdatedHandler _subscriptionUpdatedHandler;
|
|
||||||
private readonly IUpcomingInvoiceHandler _upcomingInvoiceHandler;
|
|
||||||
private readonly IChargeSucceededHandler _chargeSucceededHandler;
|
|
||||||
private readonly IChargeRefundedHandler _chargeRefundedHandler;
|
|
||||||
private readonly IPaymentSucceededHandler _paymentSucceededHandler;
|
|
||||||
private readonly IPaymentFailedHandler _paymentFailedHandler;
|
|
||||||
private readonly IInvoiceCreatedHandler _invoiceCreatedHandler;
|
|
||||||
private readonly IPaymentMethodAttachedHandler _paymentMethodAttachedHandler;
|
|
||||||
private readonly ICustomerUpdatedHandler _customerUpdatedHandler;
|
|
||||||
private readonly IInvoiceFinalizedHandler _invoiceFinalizedHandler;
|
|
||||||
|
|
||||||
public StripeEventProcessor(
|
|
||||||
ILogger<StripeEventProcessor> logger,
|
ILogger<StripeEventProcessor> logger,
|
||||||
ISubscriptionDeletedHandler subscriptionDeletedHandler,
|
ISubscriptionDeletedHandler subscriptionDeletedHandler,
|
||||||
ISubscriptionUpdatedHandler subscriptionUpdatedHandler,
|
ISubscriptionUpdatedHandler subscriptionUpdatedHandler,
|
||||||
@@ -30,61 +15,52 @@ public class StripeEventProcessor : IStripeEventProcessor
|
|||||||
IInvoiceCreatedHandler invoiceCreatedHandler,
|
IInvoiceCreatedHandler invoiceCreatedHandler,
|
||||||
IPaymentMethodAttachedHandler paymentMethodAttachedHandler,
|
IPaymentMethodAttachedHandler paymentMethodAttachedHandler,
|
||||||
ICustomerUpdatedHandler customerUpdatedHandler,
|
ICustomerUpdatedHandler customerUpdatedHandler,
|
||||||
IInvoiceFinalizedHandler invoiceFinalizedHandler)
|
IInvoiceFinalizedHandler invoiceFinalizedHandler,
|
||||||
{
|
ISetupIntentSucceededHandler setupIntentSucceededHandler)
|
||||||
_logger = logger;
|
: IStripeEventProcessor
|
||||||
_subscriptionDeletedHandler = subscriptionDeletedHandler;
|
{
|
||||||
_subscriptionUpdatedHandler = subscriptionUpdatedHandler;
|
|
||||||
_upcomingInvoiceHandler = upcomingInvoiceHandler;
|
|
||||||
_chargeSucceededHandler = chargeSucceededHandler;
|
|
||||||
_chargeRefundedHandler = chargeRefundedHandler;
|
|
||||||
_paymentSucceededHandler = paymentSucceededHandler;
|
|
||||||
_paymentFailedHandler = paymentFailedHandler;
|
|
||||||
_invoiceCreatedHandler = invoiceCreatedHandler;
|
|
||||||
_paymentMethodAttachedHandler = paymentMethodAttachedHandler;
|
|
||||||
_customerUpdatedHandler = customerUpdatedHandler;
|
|
||||||
_invoiceFinalizedHandler = invoiceFinalizedHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ProcessEventAsync(Event parsedEvent)
|
public async Task ProcessEventAsync(Event parsedEvent)
|
||||||
{
|
{
|
||||||
switch (parsedEvent.Type)
|
switch (parsedEvent.Type)
|
||||||
{
|
{
|
||||||
case HandledStripeWebhook.SubscriptionDeleted:
|
case HandledStripeWebhook.SubscriptionDeleted:
|
||||||
await _subscriptionDeletedHandler.HandleAsync(parsedEvent);
|
await subscriptionDeletedHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.SubscriptionUpdated:
|
case HandledStripeWebhook.SubscriptionUpdated:
|
||||||
await _subscriptionUpdatedHandler.HandleAsync(parsedEvent);
|
await subscriptionUpdatedHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.UpcomingInvoice:
|
case HandledStripeWebhook.UpcomingInvoice:
|
||||||
await _upcomingInvoiceHandler.HandleAsync(parsedEvent);
|
await upcomingInvoiceHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.ChargeSucceeded:
|
case HandledStripeWebhook.ChargeSucceeded:
|
||||||
await _chargeSucceededHandler.HandleAsync(parsedEvent);
|
await chargeSucceededHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.ChargeRefunded:
|
case HandledStripeWebhook.ChargeRefunded:
|
||||||
await _chargeRefundedHandler.HandleAsync(parsedEvent);
|
await chargeRefundedHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.PaymentSucceeded:
|
case HandledStripeWebhook.PaymentSucceeded:
|
||||||
await _paymentSucceededHandler.HandleAsync(parsedEvent);
|
await paymentSucceededHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.PaymentFailed:
|
case HandledStripeWebhook.PaymentFailed:
|
||||||
await _paymentFailedHandler.HandleAsync(parsedEvent);
|
await paymentFailedHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.InvoiceCreated:
|
case HandledStripeWebhook.InvoiceCreated:
|
||||||
await _invoiceCreatedHandler.HandleAsync(parsedEvent);
|
await invoiceCreatedHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.PaymentMethodAttached:
|
case HandledStripeWebhook.PaymentMethodAttached:
|
||||||
await _paymentMethodAttachedHandler.HandleAsync(parsedEvent);
|
await paymentMethodAttachedHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.CustomerUpdated:
|
case HandledStripeWebhook.CustomerUpdated:
|
||||||
await _customerUpdatedHandler.HandleAsync(parsedEvent);
|
await customerUpdatedHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.InvoiceFinalized:
|
case HandledStripeWebhook.InvoiceFinalized:
|
||||||
await _invoiceFinalizedHandler.HandleAsync(parsedEvent);
|
await invoiceFinalizedHandler.HandleAsync(parsedEvent);
|
||||||
|
break;
|
||||||
|
case HandledStripeWebhook.SetupIntentSucceeded:
|
||||||
|
await setupIntentSucceededHandler.HandleAsync(parsedEvent);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
_logger.LogWarning("Unsupported event received. {EventType}", parsedEvent.Type);
|
logger.LogWarning("Unsupported event received. {EventType}", parsedEvent.Type);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,183 +1,122 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
using Bit.Billing.Constants;
|
||||||
#nullable disable
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Billing.Constants;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Billing.Services.Implementations;
|
namespace Bit.Billing.Services.Implementations;
|
||||||
|
|
||||||
public class StripeEventService : IStripeEventService
|
public class StripeEventService(
|
||||||
{
|
|
||||||
private readonly GlobalSettings _globalSettings;
|
|
||||||
private readonly ILogger<StripeEventService> _logger;
|
|
||||||
private readonly IStripeFacade _stripeFacade;
|
|
||||||
|
|
||||||
public StripeEventService(
|
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ILogger<StripeEventService> logger,
|
IOrganizationRepository organizationRepository,
|
||||||
|
IProviderRepository providerRepository,
|
||||||
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeFacade stripeFacade)
|
IStripeFacade stripeFacade)
|
||||||
|
: IStripeEventService
|
||||||
|
{
|
||||||
|
public async Task<Charge> GetCharge(Event stripeEvent, bool fresh = false, List<string>? expand = null)
|
||||||
{
|
{
|
||||||
_globalSettings = globalSettings;
|
var charge = Extract<Charge>(stripeEvent);
|
||||||
_logger = logger;
|
|
||||||
_stripeFacade = stripeFacade;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Charge> GetCharge(Event stripeEvent, bool fresh = false, List<string> expand = null)
|
|
||||||
{
|
|
||||||
var eventCharge = Extract<Charge>(stripeEvent);
|
|
||||||
|
|
||||||
if (!fresh)
|
if (!fresh)
|
||||||
{
|
{
|
||||||
return eventCharge;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(eventCharge.Id))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Cannot retrieve up-to-date Charge for Event with ID '{eventId}' because no Charge ID was included in the Event.", stripeEvent.Id);
|
|
||||||
return eventCharge;
|
|
||||||
}
|
|
||||||
|
|
||||||
var charge = await _stripeFacade.GetCharge(eventCharge.Id, new ChargeGetOptions { Expand = expand });
|
|
||||||
|
|
||||||
if (charge == null)
|
|
||||||
{
|
|
||||||
throw new Exception(
|
|
||||||
$"Received null Charge from Stripe for ID '{eventCharge.Id}' while processing Event with ID '{stripeEvent.Id}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return charge;
|
return charge;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Customer> GetCustomer(Event stripeEvent, bool fresh = false, List<string> expand = null)
|
return await stripeFacade.GetCharge(charge.Id, new ChargeGetOptions { Expand = expand });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Customer> GetCustomer(Event stripeEvent, bool fresh = false, List<string>? expand = null)
|
||||||
{
|
{
|
||||||
var eventCustomer = Extract<Customer>(stripeEvent);
|
var customer = Extract<Customer>(stripeEvent);
|
||||||
|
|
||||||
if (!fresh)
|
if (!fresh)
|
||||||
{
|
{
|
||||||
return eventCustomer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(eventCustomer.Id))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Cannot retrieve up-to-date Customer for Event with ID '{eventId}' because no Customer ID was included in the Event.", stripeEvent.Id);
|
|
||||||
return eventCustomer;
|
|
||||||
}
|
|
||||||
|
|
||||||
var customer = await _stripeFacade.GetCustomer(eventCustomer.Id, new CustomerGetOptions { Expand = expand });
|
|
||||||
|
|
||||||
if (customer == null)
|
|
||||||
{
|
|
||||||
throw new Exception(
|
|
||||||
$"Received null Customer from Stripe for ID '{eventCustomer.Id}' while processing Event with ID '{stripeEvent.Id}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return customer;
|
return customer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Invoice> GetInvoice(Event stripeEvent, bool fresh = false, List<string> expand = null)
|
return await stripeFacade.GetCustomer(customer.Id, new CustomerGetOptions { Expand = expand });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Invoice> GetInvoice(Event stripeEvent, bool fresh = false, List<string>? expand = null)
|
||||||
{
|
{
|
||||||
var eventInvoice = Extract<Invoice>(stripeEvent);
|
var invoice = Extract<Invoice>(stripeEvent);
|
||||||
|
|
||||||
if (!fresh)
|
if (!fresh)
|
||||||
{
|
{
|
||||||
return eventInvoice;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(eventInvoice.Id))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Cannot retrieve up-to-date Invoice for Event with ID '{eventId}' because no Invoice ID was included in the Event.", stripeEvent.Id);
|
|
||||||
return eventInvoice;
|
|
||||||
}
|
|
||||||
|
|
||||||
var invoice = await _stripeFacade.GetInvoice(eventInvoice.Id, new InvoiceGetOptions { Expand = expand });
|
|
||||||
|
|
||||||
if (invoice == null)
|
|
||||||
{
|
|
||||||
throw new Exception(
|
|
||||||
$"Received null Invoice from Stripe for ID '{eventInvoice.Id}' while processing Event with ID '{stripeEvent.Id}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return invoice;
|
return invoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PaymentMethod> GetPaymentMethod(Event stripeEvent, bool fresh = false, List<string> expand = null)
|
return await stripeFacade.GetInvoice(invoice.Id, new InvoiceGetOptions { Expand = expand });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PaymentMethod> GetPaymentMethod(Event stripeEvent, bool fresh = false,
|
||||||
|
List<string>? expand = null)
|
||||||
{
|
{
|
||||||
var eventPaymentMethod = Extract<PaymentMethod>(stripeEvent);
|
var paymentMethod = Extract<PaymentMethod>(stripeEvent);
|
||||||
|
|
||||||
if (!fresh)
|
if (!fresh)
|
||||||
{
|
{
|
||||||
return eventPaymentMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(eventPaymentMethod.Id))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Cannot retrieve up-to-date Payment Method for Event with ID '{eventId}' because no Payment Method ID was included in the Event.", stripeEvent.Id);
|
|
||||||
return eventPaymentMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
var paymentMethod = await _stripeFacade.GetPaymentMethod(eventPaymentMethod.Id, new PaymentMethodGetOptions { Expand = expand });
|
|
||||||
|
|
||||||
if (paymentMethod == null)
|
|
||||||
{
|
|
||||||
throw new Exception(
|
|
||||||
$"Received null Payment Method from Stripe for ID '{eventPaymentMethod.Id}' while processing Event with ID '{stripeEvent.Id}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return paymentMethod;
|
return paymentMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Subscription> GetSubscription(Event stripeEvent, bool fresh = false, List<string> expand = null)
|
return await stripeFacade.GetPaymentMethod(paymentMethod.Id, new PaymentMethodGetOptions { Expand = expand });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SetupIntent> GetSetupIntent(Event stripeEvent, bool fresh = false, List<string>? expand = null)
|
||||||
{
|
{
|
||||||
var eventSubscription = Extract<Subscription>(stripeEvent);
|
var setupIntent = Extract<SetupIntent>(stripeEvent);
|
||||||
|
|
||||||
if (!fresh)
|
if (!fresh)
|
||||||
{
|
{
|
||||||
return eventSubscription;
|
return setupIntent;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(eventSubscription.Id))
|
return await stripeFacade.GetSetupIntent(setupIntent.Id, new SetupIntentGetOptions { Expand = expand });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Subscription> GetSubscription(Event stripeEvent, bool fresh = false, List<string>? expand = null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Cannot retrieve up-to-date Subscription for Event with ID '{eventId}' because no Subscription ID was included in the Event.", stripeEvent.Id);
|
var subscription = Extract<Subscription>(stripeEvent);
|
||||||
return eventSubscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
var subscription = await _stripeFacade.GetSubscription(eventSubscription.Id, new SubscriptionGetOptions { Expand = expand });
|
if (!fresh)
|
||||||
|
|
||||||
if (subscription == null)
|
|
||||||
{
|
{
|
||||||
throw new Exception(
|
|
||||||
$"Received null Subscription from Stripe for ID '{eventSubscription.Id}' while processing Event with ID '{stripeEvent.Id}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await stripeFacade.GetSubscription(subscription.Id, new SubscriptionGetOptions { Expand = expand });
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> ValidateCloudRegion(Event stripeEvent)
|
public async Task<bool> ValidateCloudRegion(Event stripeEvent)
|
||||||
{
|
{
|
||||||
var serverRegion = _globalSettings.BaseServiceUri.CloudRegion;
|
var serverRegion = globalSettings.BaseServiceUri.CloudRegion;
|
||||||
|
|
||||||
var customerExpansion = new List<string> { "customer" };
|
var customerExpansion = new List<string> { "customer" };
|
||||||
|
|
||||||
var customerMetadata = stripeEvent.Type switch
|
var customerMetadata = stripeEvent.Type switch
|
||||||
{
|
{
|
||||||
HandledStripeWebhook.SubscriptionDeleted or HandledStripeWebhook.SubscriptionUpdated =>
|
HandledStripeWebhook.SubscriptionDeleted or HandledStripeWebhook.SubscriptionUpdated =>
|
||||||
(await GetSubscription(stripeEvent, true, customerExpansion))?.Customer?.Metadata,
|
(await GetSubscription(stripeEvent, true, customerExpansion)).Customer?.Metadata,
|
||||||
|
|
||||||
HandledStripeWebhook.ChargeSucceeded or HandledStripeWebhook.ChargeRefunded =>
|
HandledStripeWebhook.ChargeSucceeded or HandledStripeWebhook.ChargeRefunded =>
|
||||||
(await GetCharge(stripeEvent, true, customerExpansion))?.Customer?.Metadata,
|
(await GetCharge(stripeEvent, true, customerExpansion)).Customer?.Metadata,
|
||||||
|
|
||||||
HandledStripeWebhook.UpcomingInvoice =>
|
HandledStripeWebhook.UpcomingInvoice =>
|
||||||
await GetCustomerMetadataFromUpcomingInvoiceEvent(stripeEvent),
|
await GetCustomerMetadataFromUpcomingInvoiceEvent(stripeEvent),
|
||||||
|
|
||||||
HandledStripeWebhook.PaymentSucceeded or HandledStripeWebhook.PaymentFailed or HandledStripeWebhook.InvoiceCreated or HandledStripeWebhook.InvoiceFinalized =>
|
HandledStripeWebhook.PaymentSucceeded or HandledStripeWebhook.PaymentFailed
|
||||||
(await GetInvoice(stripeEvent, true, customerExpansion))?.Customer?.Metadata,
|
or HandledStripeWebhook.InvoiceCreated or HandledStripeWebhook.InvoiceFinalized =>
|
||||||
|
(await GetInvoice(stripeEvent, true, customerExpansion)).Customer?.Metadata,
|
||||||
|
|
||||||
HandledStripeWebhook.PaymentMethodAttached =>
|
HandledStripeWebhook.PaymentMethodAttached =>
|
||||||
(await GetPaymentMethod(stripeEvent, true, customerExpansion))?.Customer?.Metadata,
|
(await GetPaymentMethod(stripeEvent, true, customerExpansion)).Customer?.Metadata,
|
||||||
|
|
||||||
HandledStripeWebhook.CustomerUpdated =>
|
HandledStripeWebhook.CustomerUpdated =>
|
||||||
(await GetCustomer(stripeEvent, true))?.Metadata,
|
(await GetCustomer(stripeEvent, true)).Metadata,
|
||||||
|
|
||||||
|
HandledStripeWebhook.SetupIntentSucceeded =>
|
||||||
|
await GetCustomerMetadataFromSetupIntentSucceededEvent(stripeEvent),
|
||||||
|
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
@@ -194,51 +133,69 @@ public class StripeEventService : IStripeEventService
|
|||||||
/* In Stripe, when we receive an invoice.upcoming event, the event does not include an Invoice ID because
|
/* In Stripe, when we receive an invoice.upcoming event, the event does not include an Invoice ID because
|
||||||
the invoice hasn't been created yet. Therefore, rather than retrieving the fresh Invoice with a 'customer'
|
the invoice hasn't been created yet. Therefore, rather than retrieving the fresh Invoice with a 'customer'
|
||||||
expansion, we need to use the Customer ID on the event to retrieve the metadata. */
|
expansion, we need to use the Customer ID on the event to retrieve the metadata. */
|
||||||
async Task<Dictionary<string, string>> GetCustomerMetadataFromUpcomingInvoiceEvent(Event localStripeEvent)
|
async Task<Dictionary<string, string>?> GetCustomerMetadataFromUpcomingInvoiceEvent(Event localStripeEvent)
|
||||||
{
|
{
|
||||||
var invoice = await GetInvoice(localStripeEvent);
|
var invoice = await GetInvoice(localStripeEvent);
|
||||||
|
|
||||||
var customer = !string.IsNullOrEmpty(invoice.CustomerId)
|
var customer = !string.IsNullOrEmpty(invoice.CustomerId)
|
||||||
? await _stripeFacade.GetCustomer(invoice.CustomerId)
|
? await stripeFacade.GetCustomer(invoice.CustomerId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return customer?.Metadata;
|
return customer?.Metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task<Dictionary<string, string>?> GetCustomerMetadataFromSetupIntentSucceededEvent(Event localStripeEvent)
|
||||||
|
{
|
||||||
|
var setupIntent = await GetSetupIntent(localStripeEvent);
|
||||||
|
|
||||||
|
var subscriberId = await setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id);
|
||||||
|
if (subscriberId == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(subscriberId.Value);
|
||||||
|
if (organization is { GatewayCustomerId: not null })
|
||||||
|
{
|
||||||
|
var organizationCustomer = await stripeFacade.GetCustomer(organization.GatewayCustomerId);
|
||||||
|
return organizationCustomer.Metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider = await providerRepository.GetByIdAsync(subscriberId.Value);
|
||||||
|
if (provider is not { GatewayCustomerId: not null })
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var providerCustomer = await stripeFacade.GetCustomer(provider.GatewayCustomerId);
|
||||||
|
return providerCustomer.Metadata;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T Extract<T>(Event stripeEvent)
|
private static T Extract<T>(Event stripeEvent)
|
||||||
{
|
=> stripeEvent.Data.Object is not T type
|
||||||
if (stripeEvent.Data.Object is not T type)
|
? throw new Exception(
|
||||||
{
|
$"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{typeof(T).Name}'")
|
||||||
throw new Exception($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{typeof(T).Name}'");
|
: type;
|
||||||
}
|
|
||||||
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetCustomerRegion(IDictionary<string, string> customerMetadata)
|
private static string GetCustomerRegion(IDictionary<string, string> customerMetadata)
|
||||||
{
|
{
|
||||||
const string defaultRegion = Core.Constants.CountryAbbreviations.UnitedStates;
|
const string defaultRegion = Core.Constants.CountryAbbreviations.UnitedStates;
|
||||||
|
|
||||||
if (customerMetadata is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customerMetadata.TryGetValue("region", out var value))
|
if (customerMetadata.TryGetValue("region", out var value))
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
var miscasedRegionKey = customerMetadata.Keys
|
var incorrectlyCasedRegionKey = customerMetadata.Keys
|
||||||
.FirstOrDefault(key => key.Equals("region", StringComparison.OrdinalIgnoreCase));
|
.FirstOrDefault(key => key.Equals("region", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (miscasedRegionKey is null)
|
if (incorrectlyCasedRegionKey is null)
|
||||||
{
|
{
|
||||||
return defaultRegion;
|
return defaultRegion;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = customerMetadata.TryGetValue(miscasedRegionKey, out var regionValue);
|
_ = customerMetadata.TryGetValue(incorrectlyCasedRegionKey, out var regionValue);
|
||||||
|
|
||||||
return !string.IsNullOrWhiteSpace(regionValue)
|
return !string.IsNullOrWhiteSpace(regionValue)
|
||||||
? regionValue
|
? regionValue
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class StripeFacade : IStripeFacade
|
|||||||
private readonly PaymentMethodService _paymentMethodService = new();
|
private readonly PaymentMethodService _paymentMethodService = new();
|
||||||
private readonly SubscriptionService _subscriptionService = new();
|
private readonly SubscriptionService _subscriptionService = new();
|
||||||
private readonly DiscountService _discountService = new();
|
private readonly DiscountService _discountService = new();
|
||||||
|
private readonly SetupIntentService _setupIntentService = new();
|
||||||
private readonly TestClockService _testClockService = new();
|
private readonly TestClockService _testClockService = new();
|
||||||
|
|
||||||
public async Task<Charge> GetCharge(
|
public async Task<Charge> GetCharge(
|
||||||
@@ -53,6 +54,13 @@ public class StripeFacade : IStripeFacade
|
|||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
await _invoiceService.GetAsync(invoiceId, invoiceGetOptions, requestOptions, cancellationToken);
|
await _invoiceService.GetAsync(invoiceId, invoiceGetOptions, requestOptions, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<SetupIntent> GetSetupIntent(
|
||||||
|
string setupIntentId,
|
||||||
|
SetupIntentGetOptions setupIntentGetOptions = null,
|
||||||
|
RequestOptions requestOptions = null,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
await _setupIntentService.GetAsync(setupIntentId, setupIntentGetOptions, requestOptions, cancellationToken);
|
||||||
|
|
||||||
public async Task<StripeList<Invoice>> ListInvoices(
|
public async Task<StripeList<Invoice>> ListInvoices(
|
||||||
InvoiceListOptions options = null,
|
InvoiceListOptions options = null,
|
||||||
RequestOptions requestOptions = null,
|
RequestOptions requestOptions = null,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Platform.Push;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
@@ -25,7 +24,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
private readonly IStripeFacade _stripeFacade;
|
private readonly IStripeFacade _stripeFacade;
|
||||||
private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand;
|
private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly ISchedulerFactory _schedulerFactory;
|
private readonly ISchedulerFactory _schedulerFactory;
|
||||||
private readonly IOrganizationEnableCommand _organizationEnableCommand;
|
private readonly IOrganizationEnableCommand _organizationEnableCommand;
|
||||||
@@ -35,6 +33,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly IProviderService _providerService;
|
private readonly IProviderService _providerService;
|
||||||
private readonly ILogger<SubscriptionUpdatedHandler> _logger;
|
private readonly ILogger<SubscriptionUpdatedHandler> _logger;
|
||||||
|
private readonly IPushNotificationAdapter _pushNotificationAdapter;
|
||||||
|
|
||||||
public SubscriptionUpdatedHandler(
|
public SubscriptionUpdatedHandler(
|
||||||
IStripeEventService stripeEventService,
|
IStripeEventService stripeEventService,
|
||||||
@@ -43,7 +42,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
IStripeFacade stripeFacade,
|
IStripeFacade stripeFacade,
|
||||||
IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand,
|
IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IPushNotificationService pushNotificationService,
|
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
ISchedulerFactory schedulerFactory,
|
ISchedulerFactory schedulerFactory,
|
||||||
IOrganizationEnableCommand organizationEnableCommand,
|
IOrganizationEnableCommand organizationEnableCommand,
|
||||||
@@ -52,7 +50,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
ILogger<SubscriptionUpdatedHandler> logger)
|
ILogger<SubscriptionUpdatedHandler> logger,
|
||||||
|
IPushNotificationAdapter pushNotificationAdapter)
|
||||||
{
|
{
|
||||||
_stripeEventService = stripeEventService;
|
_stripeEventService = stripeEventService;
|
||||||
_stripeEventUtilityService = stripeEventUtilityService;
|
_stripeEventUtilityService = stripeEventUtilityService;
|
||||||
@@ -61,7 +60,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
_stripeFacade = stripeFacade;
|
_stripeFacade = stripeFacade;
|
||||||
_organizationSponsorshipRenewCommand = organizationSponsorshipRenewCommand;
|
_organizationSponsorshipRenewCommand = organizationSponsorshipRenewCommand;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_pushNotificationService = pushNotificationService;
|
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_schedulerFactory = schedulerFactory;
|
_schedulerFactory = schedulerFactory;
|
||||||
@@ -72,6 +70,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_providerService = providerService;
|
_providerService = providerService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_pushNotificationAdapter = pushNotificationAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -125,7 +124,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
|
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
|
||||||
if (organization != null)
|
if (organization != null)
|
||||||
{
|
{
|
||||||
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);
|
await _pushNotificationAdapter.NotifyEnabledChangedAsync(organization);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ public class Startup
|
|||||||
services.AddScoped<IPaymentMethodAttachedHandler, PaymentMethodAttachedHandler>();
|
services.AddScoped<IPaymentMethodAttachedHandler, PaymentMethodAttachedHandler>();
|
||||||
services.AddScoped<IPaymentSucceededHandler, PaymentSucceededHandler>();
|
services.AddScoped<IPaymentSucceededHandler, PaymentSucceededHandler>();
|
||||||
services.AddScoped<IInvoiceFinalizedHandler, InvoiceFinalizedHandler>();
|
services.AddScoped<IInvoiceFinalizedHandler, InvoiceFinalizedHandler>();
|
||||||
|
services.AddScoped<ISetupIntentSucceededHandler, SetupIntentSucceededHandler>();
|
||||||
services.AddScoped<IStripeEventProcessor, StripeEventProcessor>();
|
services.AddScoped<IStripeEventProcessor, StripeEventProcessor>();
|
||||||
|
|
||||||
// Identity
|
// Identity
|
||||||
@@ -111,6 +112,7 @@ public class Startup
|
|||||||
services.AddScoped<IStripeFacade, StripeFacade>();
|
services.AddScoped<IStripeFacade, StripeFacade>();
|
||||||
services.AddScoped<IStripeEventService, StripeEventService>();
|
services.AddScoped<IStripeEventService, StripeEventService>();
|
||||||
services.AddScoped<IProviderEventService, ProviderEventService>();
|
services.AddScoped<IProviderEventService, ProviderEventService>();
|
||||||
|
services.AddScoped<IPushNotificationAdapter, PushNotificationAdapter>();
|
||||||
|
|
||||||
// Add Quartz services first
|
// Add Quartz services first
|
||||||
services.AddQuartz(q =>
|
services.AddQuartz(q =>
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
public interface ISetupIntentCache
|
public interface ISetupIntentCache
|
||||||
{
|
{
|
||||||
Task<string> Get(Guid subscriberId);
|
Task<string?> GetSetupIntentIdForSubscriber(Guid subscriberId);
|
||||||
|
Task<Guid?> GetSubscriberIdForSetupIntent(string setupIntentId);
|
||||||
Task Remove(Guid subscriberId);
|
Task RemoveSetupIntentForSubscriber(Guid subscriberId);
|
||||||
|
|
||||||
Task Set(Guid subscriberId, string setupIntentId);
|
Task Set(Guid subscriberId, string setupIntentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Caches.Implementations;
|
namespace Bit.Core.Billing.Caches.Implementations;
|
||||||
@@ -10,26 +7,41 @@ public class SetupIntentDistributedCache(
|
|||||||
[FromKeyedServices("persistent")]
|
[FromKeyedServices("persistent")]
|
||||||
IDistributedCache distributedCache) : ISetupIntentCache
|
IDistributedCache distributedCache) : ISetupIntentCache
|
||||||
{
|
{
|
||||||
public async Task<string> Get(Guid subscriberId)
|
public async Task<string?> GetSetupIntentIdForSubscriber(Guid subscriberId)
|
||||||
{
|
{
|
||||||
var cacheKey = GetCacheKey(subscriberId);
|
var cacheKey = GetCacheKeyBySubscriberId(subscriberId);
|
||||||
|
|
||||||
return await distributedCache.GetStringAsync(cacheKey);
|
return await distributedCache.GetStringAsync(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Remove(Guid subscriberId)
|
public async Task<Guid?> GetSubscriberIdForSetupIntent(string setupIntentId)
|
||||||
{
|
{
|
||||||
var cacheKey = GetCacheKey(subscriberId);
|
var cacheKey = GetCacheKeyBySetupIntentId(setupIntentId);
|
||||||
|
var value = await distributedCache.GetStringAsync(cacheKey);
|
||||||
|
if (string.IsNullOrEmpty(value) || !Guid.TryParse(value, out var subscriberId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return subscriberId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveSetupIntentForSubscriber(Guid subscriberId)
|
||||||
|
{
|
||||||
|
var cacheKey = GetCacheKeyBySubscriberId(subscriberId);
|
||||||
await distributedCache.RemoveAsync(cacheKey);
|
await distributedCache.RemoveAsync(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Set(Guid subscriberId, string setupIntentId)
|
public async Task Set(Guid subscriberId, string setupIntentId)
|
||||||
{
|
{
|
||||||
var cacheKey = GetCacheKey(subscriberId);
|
var bySubscriberIdCacheKey = GetCacheKeyBySubscriberId(subscriberId);
|
||||||
|
var bySetupIntentIdCacheKey = GetCacheKeyBySetupIntentId(setupIntentId);
|
||||||
await distributedCache.SetStringAsync(cacheKey, setupIntentId);
|
await Task.WhenAll(
|
||||||
|
distributedCache.SetStringAsync(bySubscriberIdCacheKey, setupIntentId),
|
||||||
|
distributedCache.SetStringAsync(bySetupIntentIdCacheKey, subscriberId.ToString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetCacheKey(Guid subscriberId) => $"pending_bank_account_{subscriberId}";
|
private static string GetCacheKeyBySetupIntentId(string setupIntentId) =>
|
||||||
|
$"subscriber_id_for_setup_intent_id_{setupIntentId}";
|
||||||
|
|
||||||
|
private static string GetCacheKeyBySubscriberId(Guid subscriberId) =>
|
||||||
|
$"setup_intent_id_for_subscriber_id_{subscriberId}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ public class GetOrganizationWarningsQuery(
|
|||||||
private async Task<bool> HasUnverifiedBankAccountAsync(
|
private async Task<bool> HasUnverifiedBankAccountAsync(
|
||||||
Organization organization)
|
Organization organization)
|
||||||
{
|
{
|
||||||
var setupIntentId = await setupIntentCache.Get(organization.Id);
|
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(setupIntentId))
|
if (string.IsNullOrEmpty(setupIntentId))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ public class OrganizationBillingService(
|
|||||||
{
|
{
|
||||||
case PaymentMethodType.BankAccount:
|
case PaymentMethodType.BankAccount:
|
||||||
{
|
{
|
||||||
await setupIntentCache.Remove(organization.Id);
|
await setupIntentCache.RemoveSetupIntentForSubscriber(organization.Id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
using Bit.Core.Billing.Caches;
|
|
||||||
using Bit.Core.Billing.Commands;
|
|
||||||
using Bit.Core.Billing.Payment.Models;
|
|
||||||
using Bit.Core.Entities;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Stripe;
|
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Payment.Commands;
|
|
||||||
|
|
||||||
public interface IVerifyBankAccountCommand
|
|
||||||
{
|
|
||||||
Task<BillingCommandResult<MaskedPaymentMethod>> Run(
|
|
||||||
ISubscriber subscriber,
|
|
||||||
string descriptorCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class VerifyBankAccountCommand(
|
|
||||||
ILogger<VerifyBankAccountCommand> logger,
|
|
||||||
ISetupIntentCache setupIntentCache,
|
|
||||||
IStripeAdapter stripeAdapter) : BaseBillingCommand<VerifyBankAccountCommand>(logger), IVerifyBankAccountCommand
|
|
||||||
{
|
|
||||||
private readonly ILogger<VerifyBankAccountCommand> _logger = logger;
|
|
||||||
|
|
||||||
protected override Conflict DefaultConflict
|
|
||||||
=> new("We had a problem verifying your bank account. Please contact support for assistance.");
|
|
||||||
|
|
||||||
public Task<BillingCommandResult<MaskedPaymentMethod>> Run(
|
|
||||||
ISubscriber subscriber,
|
|
||||||
string descriptorCode) => HandleAsync<MaskedPaymentMethod>(async () =>
|
|
||||||
{
|
|
||||||
var setupIntentId = await setupIntentCache.Get(subscriber.Id);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(setupIntentId))
|
|
||||||
{
|
|
||||||
_logger.LogError(
|
|
||||||
"{Command}: Could not find setup intent to verify subscriber's ({SubscriberID}) bank account",
|
|
||||||
CommandName, subscriber.Id);
|
|
||||||
return DefaultConflict;
|
|
||||||
}
|
|
||||||
|
|
||||||
await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId,
|
|
||||||
new SetupIntentVerifyMicrodepositsOptions { DescriptorCode = descriptorCode });
|
|
||||||
|
|
||||||
var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId,
|
|
||||||
new SetupIntentGetOptions { Expand = ["payment_method"] });
|
|
||||||
|
|
||||||
var paymentMethod = await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
|
|
||||||
new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId });
|
|
||||||
|
|
||||||
await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId,
|
|
||||||
new CustomerUpdateOptions
|
|
||||||
{
|
|
||||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
|
||||||
{
|
|
||||||
DefaultPaymentMethod = setupIntent.PaymentMethodId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return MaskedPaymentMethod.From(paymentMethod.UsBankAccount);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ public record MaskedBankAccount
|
|||||||
{
|
{
|
||||||
public required string BankName { get; init; }
|
public required string BankName { get; init; }
|
||||||
public required string Last4 { get; init; }
|
public required string Last4 { get; init; }
|
||||||
public required bool Verified { get; init; }
|
public string? HostedVerificationUrl { get; init; }
|
||||||
public string Type => "bankAccount";
|
public string Type => "bankAccount";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +39,7 @@ public class MaskedPaymentMethod(OneOf<MaskedBankAccount, MaskedCard, MaskedPayP
|
|||||||
public static MaskedPaymentMethod From(BankAccount bankAccount) => new MaskedBankAccount
|
public static MaskedPaymentMethod From(BankAccount bankAccount) => new MaskedBankAccount
|
||||||
{
|
{
|
||||||
BankName = bankAccount.BankName,
|
BankName = bankAccount.BankName,
|
||||||
Last4 = bankAccount.Last4,
|
Last4 = bankAccount.Last4
|
||||||
Verified = bankAccount.Status == "verified"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static MaskedPaymentMethod From(Card card) => new MaskedCard
|
public static MaskedPaymentMethod From(Card card) => new MaskedCard
|
||||||
@@ -61,7 +60,7 @@ public class MaskedPaymentMethod(OneOf<MaskedBankAccount, MaskedCard, MaskedPayP
|
|||||||
{
|
{
|
||||||
BankName = setupIntent.PaymentMethod.UsBankAccount.BankName,
|
BankName = setupIntent.PaymentMethod.UsBankAccount.BankName,
|
||||||
Last4 = setupIntent.PaymentMethod.UsBankAccount.Last4,
|
Last4 = setupIntent.PaymentMethod.UsBankAccount.Last4,
|
||||||
Verified = false
|
HostedVerificationUrl = setupIntent.NextAction?.VerifyWithMicrodeposits?.HostedVerificationUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
public static MaskedPaymentMethod From(SourceCard sourceCard) => new MaskedCard
|
public static MaskedPaymentMethod From(SourceCard sourceCard) => new MaskedCard
|
||||||
@@ -74,8 +73,7 @@ public class MaskedPaymentMethod(OneOf<MaskedBankAccount, MaskedCard, MaskedPayP
|
|||||||
public static MaskedPaymentMethod From(PaymentMethodUsBankAccount bankAccount) => new MaskedBankAccount
|
public static MaskedPaymentMethod From(PaymentMethodUsBankAccount bankAccount) => new MaskedBankAccount
|
||||||
{
|
{
|
||||||
BankName = bankAccount.BankName,
|
BankName = bankAccount.BankName,
|
||||||
Last4 = bankAccount.Last4,
|
Last4 = bankAccount.Last4
|
||||||
Verified = true
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static MaskedPaymentMethod From(PayPalAccount payPalAccount) => new MaskedPayPalAccount { Email = payPalAccount.Email };
|
public static MaskedPaymentMethod From(PayPalAccount payPalAccount) => new MaskedPayPalAccount { Email = payPalAccount.Email };
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ public class GetPaymentMethodQuery(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First check for PayPal
|
||||||
if (customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId))
|
if (customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId))
|
||||||
{
|
{
|
||||||
var braintreeCustomer = await braintreeGateway.Customer.FindAsync(braintreeCustomerId);
|
var braintreeCustomer = await braintreeGateway.Customer.FindAsync(braintreeCustomerId);
|
||||||
@@ -47,6 +48,23 @@ public class GetPaymentMethodQuery(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then check for a bank account pending verification
|
||||||
|
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriber.Id);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(setupIntentId))
|
||||||
|
{
|
||||||
|
var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
|
||||||
|
{
|
||||||
|
Expand = ["payment_method"]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (setupIntent.IsUnverifiedBankAccount())
|
||||||
|
{
|
||||||
|
return MaskedPaymentMethod.From(setupIntent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check the default payment method
|
||||||
var paymentMethod = customer.InvoiceSettings.DefaultPaymentMethod != null
|
var paymentMethod = customer.InvoiceSettings.DefaultPaymentMethod != null
|
||||||
? customer.InvoiceSettings.DefaultPaymentMethod.Type switch
|
? customer.InvoiceSettings.DefaultPaymentMethod.Type switch
|
||||||
{
|
{
|
||||||
@@ -61,40 +79,12 @@ public class GetPaymentMethodQuery(
|
|||||||
return paymentMethod;
|
return paymentMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customer.DefaultSource != null)
|
return customer.DefaultSource switch
|
||||||
{
|
|
||||||
paymentMethod = customer.DefaultSource switch
|
|
||||||
{
|
{
|
||||||
Card card => MaskedPaymentMethod.From(card),
|
Card card => MaskedPaymentMethod.From(card),
|
||||||
BankAccount bankAccount => MaskedPaymentMethod.From(bankAccount),
|
BankAccount bankAccount => MaskedPaymentMethod.From(bankAccount),
|
||||||
Source { Card: not null } source => MaskedPaymentMethod.From(source.Card),
|
Source { Card: not null } source => MaskedPaymentMethod.From(source.Card),
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (paymentMethod != null)
|
|
||||||
{
|
|
||||||
return paymentMethod;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var setupIntentId = await setupIntentCache.Get(subscriber.Id);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(setupIntentId))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
|
|
||||||
{
|
|
||||||
Expand = ["payment_method"]
|
|
||||||
});
|
|
||||||
|
|
||||||
// ReSharper disable once ConvertIfStatementToReturnStatement
|
|
||||||
if (!setupIntent.IsUnverifiedBankAccount())
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MaskedPaymentMethod.From(setupIntent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ public static class Registrations
|
|||||||
services.AddTransient<ICreateBitPayInvoiceForCreditCommand, CreateBitPayInvoiceForCreditCommand>();
|
services.AddTransient<ICreateBitPayInvoiceForCreditCommand, CreateBitPayInvoiceForCreditCommand>();
|
||||||
services.AddTransient<IUpdateBillingAddressCommand, UpdateBillingAddressCommand>();
|
services.AddTransient<IUpdateBillingAddressCommand, UpdateBillingAddressCommand>();
|
||||||
services.AddTransient<IUpdatePaymentMethodCommand, UpdatePaymentMethodCommand>();
|
services.AddTransient<IUpdatePaymentMethodCommand, UpdatePaymentMethodCommand>();
|
||||||
services.AddTransient<IVerifyBankAccountCommand, VerifyBankAccountCommand>();
|
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
services.AddTransient<IGetBillingAddressQuery, GetBillingAddressQuery>();
|
services.AddTransient<IGetBillingAddressQuery, GetBillingAddressQuery>();
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ public class PremiumUserBillingService(
|
|||||||
{
|
{
|
||||||
case PaymentMethodType.BankAccount:
|
case PaymentMethodType.BankAccount:
|
||||||
{
|
{
|
||||||
await setupIntentCache.Remove(user.Id);
|
await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||||
|
|||||||
@@ -858,7 +858,7 @@ public class SubscriberService(
|
|||||||
ISubscriber subscriber,
|
ISubscriber subscriber,
|
||||||
string descriptorCode)
|
string descriptorCode)
|
||||||
{
|
{
|
||||||
var setupIntentId = await setupIntentCache.Get(subscriber.Id);
|
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriber.Id);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(setupIntentId))
|
if (string.IsNullOrEmpty(setupIntentId))
|
||||||
{
|
{
|
||||||
@@ -986,7 +986,7 @@ public class SubscriberService(
|
|||||||
* attachedPaymentMethodDTO being null represents a case where we could be looking for the SetupIntent for an unverified "us_bank_account".
|
* attachedPaymentMethodDTO being null represents a case where we could be looking for the SetupIntent for an unverified "us_bank_account".
|
||||||
* We store the ID of this SetupIntent in the cache when we originally update the payment method.
|
* We store the ID of this SetupIntent in the cache when we originally update the payment method.
|
||||||
*/
|
*/
|
||||||
var setupIntentId = await setupIntentCache.Get(subscriberId);
|
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriberId);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(setupIntentId))
|
if (string.IsNullOrEmpty(setupIntentId))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -86,3 +86,14 @@ public class OrganizationCollectionManagementPushNotification
|
|||||||
public bool LimitCollectionDeletion { get; init; }
|
public bool LimitCollectionDeletion { get; init; }
|
||||||
public bool LimitItemDeletion { get; init; }
|
public bool LimitItemDeletion { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class OrganizationBankAccountVerifiedPushNotification
|
||||||
|
{
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProviderBankAccountVerifiedPushNotification
|
||||||
|
{
|
||||||
|
public Guid ProviderId { get; set; }
|
||||||
|
public Guid AdminId { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -399,20 +399,6 @@ public interface IPushNotificationService
|
|||||||
ExcludeCurrentContext = true,
|
ExcludeCurrentContext = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Task PushSyncOrganizationStatusAsync(Organization organization)
|
|
||||||
=> PushAsync(new PushNotification<OrganizationStatusPushNotification>
|
|
||||||
{
|
|
||||||
Type = PushType.SyncOrganizationStatusChanged,
|
|
||||||
Target = NotificationTarget.Organization,
|
|
||||||
TargetId = organization.Id,
|
|
||||||
Payload = new OrganizationStatusPushNotification
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
Enabled = organization.Enabled,
|
|
||||||
},
|
|
||||||
ExcludeCurrentContext = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization)
|
Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization)
|
||||||
=> PushAsync(new PushNotification<OrganizationCollectionManagementPushNotification>
|
=> PushAsync(new PushNotification<OrganizationCollectionManagementPushNotification>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -90,4 +90,10 @@ public enum PushType : byte
|
|||||||
|
|
||||||
[NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.UserPushNotification))]
|
[NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.UserPushNotification))]
|
||||||
RefreshSecurityTasks = 22,
|
RefreshSecurityTasks = 22,
|
||||||
|
|
||||||
|
[NotificationInfo("@bitwarden/team-billing-dev", typeof(Models.ProviderBankAccountVerifiedPushNotification))]
|
||||||
|
OrganizationBankAccountVerified = 23,
|
||||||
|
|
||||||
|
[NotificationInfo("@bitwarden/team-billing-dev", typeof(Models.ProviderBankAccountVerifiedPushNotification))]
|
||||||
|
ProviderBankAccountVerified = 24
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,20 @@ public static class HubHelpers
|
|||||||
await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(organizationCollectionSettingsChangedNotification.Payload.OrganizationId))
|
await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(organizationCollectionSettingsChangedNotification.Payload.OrganizationId))
|
||||||
.SendAsync(_receiveMessageMethod, organizationCollectionSettingsChangedNotification, cancellationToken);
|
.SendAsync(_receiveMessageMethod, organizationCollectionSettingsChangedNotification, cancellationToken);
|
||||||
break;
|
break;
|
||||||
|
case PushType.OrganizationBankAccountVerified:
|
||||||
|
var organizationBankAccountVerifiedNotification =
|
||||||
|
JsonSerializer.Deserialize<PushNotificationData<OrganizationBankAccountVerifiedPushNotification>>(
|
||||||
|
notificationJson, _deserializerOptions);
|
||||||
|
await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(organizationBankAccountVerifiedNotification.Payload.OrganizationId))
|
||||||
|
.SendAsync(_receiveMessageMethod, organizationBankAccountVerifiedNotification, cancellationToken);
|
||||||
|
break;
|
||||||
|
case PushType.ProviderBankAccountVerified:
|
||||||
|
var providerBankAccountVerifiedNotification =
|
||||||
|
JsonSerializer.Deserialize<PushNotificationData<ProviderBankAccountVerifiedPushNotification>>(
|
||||||
|
notificationJson, _deserializerOptions);
|
||||||
|
await hubContext.Clients.User(providerBankAccountVerifiedNotification.Payload.AdminId.ToString())
|
||||||
|
.SendAsync(_receiveMessageMethod, providerBankAccountVerifiedNotification, cancellationToken);
|
||||||
|
break;
|
||||||
case PushType.Notification:
|
case PushType.Notification:
|
||||||
case PushType.NotificationStatus:
|
case PushType.NotificationStatus:
|
||||||
var notificationData = JsonSerializer.Deserialize<PushNotificationData<NotificationPushNotification>>(
|
var notificationData = JsonSerializer.Deserialize<PushNotificationData<NotificationPushNotification>>(
|
||||||
@@ -144,6 +158,7 @@ public static class HubHelpers
|
|||||||
.SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken);
|
.SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
logger.LogWarning("Notification type '{NotificationType}' has not been registered in HubHelpers and will not be pushed as as result", notification.Type);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
242
test/Billing.Test/Services/SetupIntentSucceededHandlerTests.cs
Normal file
242
test/Billing.Test/Services/SetupIntentSucceededHandlerTests.cs
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
using Bit.Billing.Services;
|
||||||
|
using Bit.Billing.Services.Implementations;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Caches;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using NSubstitute;
|
||||||
|
using Stripe;
|
||||||
|
using Xunit;
|
||||||
|
using Event = Stripe.Event;
|
||||||
|
|
||||||
|
namespace Bit.Billing.Test.Services;
|
||||||
|
|
||||||
|
public class SetupIntentSucceededHandlerTests
|
||||||
|
{
|
||||||
|
private static readonly Event _mockEvent = new() { Id = "evt_test", Type = "setup_intent.succeeded" };
|
||||||
|
private static readonly string[] _expand = ["payment_method"];
|
||||||
|
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IProviderRepository _providerRepository;
|
||||||
|
private readonly IPushNotificationAdapter _pushNotificationAdapter;
|
||||||
|
private readonly ISetupIntentCache _setupIntentCache;
|
||||||
|
private readonly IStripeAdapter _stripeAdapter;
|
||||||
|
private readonly IStripeEventService _stripeEventService;
|
||||||
|
private readonly SetupIntentSucceededHandler _handler;
|
||||||
|
|
||||||
|
public SetupIntentSucceededHandlerTests()
|
||||||
|
{
|
||||||
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
|
_providerRepository = Substitute.For<IProviderRepository>();
|
||||||
|
_pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();
|
||||||
|
_setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||||
|
_stripeAdapter = Substitute.For<IStripeAdapter>();
|
||||||
|
_stripeEventService = Substitute.For<IStripeEventService>();
|
||||||
|
|
||||||
|
_handler = new SetupIntentSucceededHandler(
|
||||||
|
_organizationRepository,
|
||||||
|
_providerRepository,
|
||||||
|
_pushNotificationAdapter,
|
||||||
|
_setupIntentCache,
|
||||||
|
_stripeAdapter,
|
||||||
|
_stripeEventService);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_PaymentMethodNotUSBankAccount_Returns()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var setupIntent = CreateSetupIntent(hasUSBankAccount: false);
|
||||||
|
|
||||||
|
_stripeEventService.GetSetupIntent(
|
||||||
|
_mockEvent,
|
||||||
|
true,
|
||||||
|
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||||
|
.Returns(setupIntent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(_mockEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _setupIntentCache.DidNotReceiveWithAnyArgs().GetSubscriberIdForSetupIntent(Arg.Any<string>());
|
||||||
|
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
|
||||||
|
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
|
||||||
|
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
|
||||||
|
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_NoSubscriberIdInCache_Returns()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var setupIntent = CreateSetupIntent();
|
||||||
|
|
||||||
|
_stripeEventService.GetSetupIntent(
|
||||||
|
_mockEvent,
|
||||||
|
true,
|
||||||
|
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||||
|
.Returns(setupIntent);
|
||||||
|
|
||||||
|
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
|
||||||
|
.Returns((Guid?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(_mockEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
|
||||||
|
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
|
||||||
|
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
|
||||||
|
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_ValidOrganization_AttachesPaymentMethodAndSendsNotification()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var organization = new Organization { Id = organizationId, Name = "Test Org", GatewayCustomerId = "cus_test" };
|
||||||
|
var setupIntent = CreateSetupIntent();
|
||||||
|
|
||||||
|
_stripeEventService.GetSetupIntent(
|
||||||
|
_mockEvent,
|
||||||
|
true,
|
||||||
|
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||||
|
.Returns(setupIntent);
|
||||||
|
|
||||||
|
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
|
||||||
|
.Returns(organizationId);
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(_mockEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeAdapter.Received(1).PaymentMethodAttachAsync(
|
||||||
|
"pm_test",
|
||||||
|
Arg.Is<PaymentMethodAttachOptions>(o => o.Customer == organization.GatewayCustomerId));
|
||||||
|
|
||||||
|
await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(organization);
|
||||||
|
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_ValidProvider_AttachesPaymentMethodAndSendsNotification()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerId = Guid.NewGuid();
|
||||||
|
var provider = new Provider { Id = providerId, Name = "Test Provider", GatewayCustomerId = "cus_test" };
|
||||||
|
var setupIntent = CreateSetupIntent();
|
||||||
|
|
||||||
|
_stripeEventService.GetSetupIntent(
|
||||||
|
_mockEvent,
|
||||||
|
true,
|
||||||
|
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||||
|
.Returns(setupIntent);
|
||||||
|
|
||||||
|
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
|
||||||
|
.Returns(providerId);
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(providerId)
|
||||||
|
.Returns((Organization?)null);
|
||||||
|
|
||||||
|
_providerRepository.GetByIdAsync(providerId)
|
||||||
|
.Returns(provider);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(_mockEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeAdapter.Received(1).PaymentMethodAttachAsync(
|
||||||
|
"pm_test",
|
||||||
|
Arg.Is<PaymentMethodAttachOptions>(o => o.Customer == provider.GatewayCustomerId));
|
||||||
|
|
||||||
|
await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(provider);
|
||||||
|
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_OrganizationWithoutGatewayCustomerId_DoesNotAttachPaymentMethod()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var organization = new Organization { Id = organizationId, Name = "Test Org", GatewayCustomerId = null };
|
||||||
|
var setupIntent = CreateSetupIntent();
|
||||||
|
|
||||||
|
_stripeEventService.GetSetupIntent(
|
||||||
|
_mockEvent,
|
||||||
|
true,
|
||||||
|
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||||
|
.Returns(setupIntent);
|
||||||
|
|
||||||
|
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
|
||||||
|
.Returns(organizationId);
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(_mockEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
|
||||||
|
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
|
||||||
|
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
|
||||||
|
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_ProviderWithoutGatewayCustomerId_DoesNotAttachPaymentMethod()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerId = Guid.NewGuid();
|
||||||
|
var provider = new Provider { Id = providerId, Name = "Test Provider", GatewayCustomerId = null };
|
||||||
|
var setupIntent = CreateSetupIntent();
|
||||||
|
|
||||||
|
_stripeEventService.GetSetupIntent(
|
||||||
|
_mockEvent,
|
||||||
|
true,
|
||||||
|
Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))
|
||||||
|
.Returns(setupIntent);
|
||||||
|
|
||||||
|
_setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id)
|
||||||
|
.Returns(providerId);
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(providerId)
|
||||||
|
.Returns((Organization?)null);
|
||||||
|
|
||||||
|
_providerRepository.GetByIdAsync(providerId)
|
||||||
|
.Returns(provider);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(_mockEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync(
|
||||||
|
Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());
|
||||||
|
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());
|
||||||
|
await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SetupIntent CreateSetupIntent(bool hasUSBankAccount = true)
|
||||||
|
{
|
||||||
|
var paymentMethod = new PaymentMethod
|
||||||
|
{
|
||||||
|
Id = "pm_test",
|
||||||
|
Type = "us_bank_account",
|
||||||
|
UsBankAccount = hasUSBankAccount ? new PaymentMethodUsBankAccount() : null
|
||||||
|
};
|
||||||
|
|
||||||
|
var setupIntent = new SetupIntent
|
||||||
|
{
|
||||||
|
Id = "seti_test",
|
||||||
|
PaymentMethod = paymentMethod
|
||||||
|
};
|
||||||
|
|
||||||
|
return setupIntent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
using Bit.Billing.Services;
|
using Bit.Billing.Services;
|
||||||
using Bit.Billing.Services.Implementations;
|
using Bit.Billing.Services.Implementations;
|
||||||
using Bit.Billing.Test.Utilities;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Caches;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -11,6 +12,9 @@ namespace Bit.Billing.Test.Services;
|
|||||||
|
|
||||||
public class StripeEventServiceTests
|
public class StripeEventServiceTests
|
||||||
{
|
{
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IProviderRepository _providerRepository;
|
||||||
|
private readonly ISetupIntentCache _setupIntentCache;
|
||||||
private readonly IStripeFacade _stripeFacade;
|
private readonly IStripeFacade _stripeFacade;
|
||||||
private readonly StripeEventService _stripeEventService;
|
private readonly StripeEventService _stripeEventService;
|
||||||
|
|
||||||
@@ -20,8 +24,11 @@ public class StripeEventServiceTests
|
|||||||
var baseServiceUriSettings = new GlobalSettings.BaseServiceUriSettings(globalSettings) { CloudRegion = "US" };
|
var baseServiceUriSettings = new GlobalSettings.BaseServiceUriSettings(globalSettings) { CloudRegion = "US" };
|
||||||
globalSettings.BaseServiceUri = baseServiceUriSettings;
|
globalSettings.BaseServiceUri = baseServiceUriSettings;
|
||||||
|
|
||||||
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
|
_providerRepository = Substitute.For<IProviderRepository>();
|
||||||
|
_setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||||
_stripeFacade = Substitute.For<IStripeFacade>();
|
_stripeFacade = Substitute.For<IStripeFacade>();
|
||||||
_stripeEventService = new StripeEventService(globalSettings, Substitute.For<ILogger<StripeEventService>>(), _stripeFacade);
|
_stripeEventService = new StripeEventService(globalSettings, _organizationRepository, _providerRepository, _setupIntentCache, _stripeFacade);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region GetCharge
|
#region GetCharge
|
||||||
@@ -29,50 +36,44 @@ public class StripeEventServiceTests
|
|||||||
public async Task GetCharge_EventNotChargeRelated_ThrowsException()
|
public async Task GetCharge_EventNotChargeRelated_ThrowsException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
|
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", new Invoice { Id = "in_test" });
|
||||||
|
|
||||||
// Act
|
// Act & Assert
|
||||||
var function = async () => await _stripeEventService.GetCharge(stripeEvent);
|
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetCharge(stripeEvent));
|
||||||
|
|
||||||
// Assert
|
|
||||||
var exception = await Assert.ThrowsAsync<Exception>(function);
|
|
||||||
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'", exception.Message);
|
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'", exception.Message);
|
||||||
|
|
||||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<ChargeGetOptions>(),
|
Arg.Any<ChargeGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetCharge_NotFresh_ReturnsEventCharge()
|
public async Task GetCharge_NotFresh_ReturnsEventCharge()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);
|
var mockCharge = new Charge { Id = "ch_test", Amount = 1000 };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", mockCharge);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var charge = await _stripeEventService.GetCharge(stripeEvent);
|
var charge = await _stripeEventService.GetCharge(stripeEvent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equivalent(stripeEvent.Data.Object as Charge, charge, true);
|
Assert.Equal(mockCharge.Id, charge.Id);
|
||||||
|
Assert.Equal(mockCharge.Amount, charge.Amount);
|
||||||
|
|
||||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<ChargeGetOptions>(),
|
Arg.Any<ChargeGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetCharge_Fresh_Expand_ReturnsAPICharge()
|
public async Task GetCharge_Fresh_Expand_ReturnsAPICharge()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);
|
var eventCharge = new Charge { Id = "ch_test", Amount = 1000 };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", eventCharge);
|
||||||
|
|
||||||
var eventCharge = stripeEvent.Data.Object as Charge;
|
var apiCharge = new Charge { Id = "ch_test", Amount = 2000 };
|
||||||
|
|
||||||
var apiCharge = Copy(eventCharge);
|
|
||||||
|
|
||||||
var expand = new List<string> { "customer" };
|
var expand = new List<string> { "customer" };
|
||||||
|
|
||||||
@@ -90,9 +91,7 @@ public class StripeEventServiceTests
|
|||||||
|
|
||||||
await _stripeFacade.Received().GetCharge(
|
await _stripeFacade.Received().GetCharge(
|
||||||
apiCharge.Id,
|
apiCharge.Id,
|
||||||
Arg.Is<ChargeGetOptions>(options => options.Expand == expand),
|
Arg.Is<ChargeGetOptions>(options => options.Expand == expand));
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -101,50 +100,44 @@ public class StripeEventServiceTests
|
|||||||
public async Task GetCustomer_EventNotCustomerRelated_ThrowsException()
|
public async Task GetCustomer_EventNotCustomerRelated_ThrowsException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
|
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", new Invoice { Id = "in_test" });
|
||||||
|
|
||||||
// Act
|
// Act & Assert
|
||||||
var function = async () => await _stripeEventService.GetCustomer(stripeEvent);
|
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetCustomer(stripeEvent));
|
||||||
|
|
||||||
// Assert
|
|
||||||
var exception = await Assert.ThrowsAsync<Exception>(function);
|
|
||||||
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'", exception.Message);
|
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'", exception.Message);
|
||||||
|
|
||||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<CustomerGetOptions>(),
|
Arg.Any<CustomerGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetCustomer_NotFresh_ReturnsEventCustomer()
|
public async Task GetCustomer_NotFresh_ReturnsEventCustomer()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
|
var mockCustomer = new Customer { Id = "cus_test", Email = "test@example.com" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", mockCustomer);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var customer = await _stripeEventService.GetCustomer(stripeEvent);
|
var customer = await _stripeEventService.GetCustomer(stripeEvent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equivalent(stripeEvent.Data.Object as Customer, customer, true);
|
Assert.Equal(mockCustomer.Id, customer.Id);
|
||||||
|
Assert.Equal(mockCustomer.Email, customer.Email);
|
||||||
|
|
||||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<CustomerGetOptions>(),
|
Arg.Any<CustomerGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetCustomer_Fresh_Expand_ReturnsAPICustomer()
|
public async Task GetCustomer_Fresh_Expand_ReturnsAPICustomer()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
|
var eventCustomer = new Customer { Id = "cus_test", Email = "test@example.com" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", eventCustomer);
|
||||||
|
|
||||||
var eventCustomer = stripeEvent.Data.Object as Customer;
|
var apiCustomer = new Customer { Id = "cus_test", Email = "updated@example.com" };
|
||||||
|
|
||||||
var apiCustomer = Copy(eventCustomer);
|
|
||||||
|
|
||||||
var expand = new List<string> { "subscriptions" };
|
var expand = new List<string> { "subscriptions" };
|
||||||
|
|
||||||
@@ -162,9 +155,7 @@ public class StripeEventServiceTests
|
|||||||
|
|
||||||
await _stripeFacade.Received().GetCustomer(
|
await _stripeFacade.Received().GetCustomer(
|
||||||
apiCustomer.Id,
|
apiCustomer.Id,
|
||||||
Arg.Is<CustomerGetOptions>(options => options.Expand == expand),
|
Arg.Is<CustomerGetOptions>(options => options.Expand == expand));
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -173,50 +164,44 @@ public class StripeEventServiceTests
|
|||||||
public async Task GetInvoice_EventNotInvoiceRelated_ThrowsException()
|
public async Task GetInvoice_EventNotInvoiceRelated_ThrowsException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
|
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
|
||||||
|
|
||||||
// Act
|
// Act & Assert
|
||||||
var function = async () => await _stripeEventService.GetInvoice(stripeEvent);
|
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetInvoice(stripeEvent));
|
||||||
|
|
||||||
// Assert
|
|
||||||
var exception = await Assert.ThrowsAsync<Exception>(function);
|
|
||||||
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'", exception.Message);
|
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'", exception.Message);
|
||||||
|
|
||||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<InvoiceGetOptions>(),
|
Arg.Any<InvoiceGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetInvoice_NotFresh_ReturnsEventInvoice()
|
public async Task GetInvoice_NotFresh_ReturnsEventInvoice()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
|
var mockInvoice = new Invoice { Id = "in_test", AmountDue = 1000 };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", mockInvoice);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var invoice = await _stripeEventService.GetInvoice(stripeEvent);
|
var invoice = await _stripeEventService.GetInvoice(stripeEvent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equivalent(stripeEvent.Data.Object as Invoice, invoice, true);
|
Assert.Equal(mockInvoice.Id, invoice.Id);
|
||||||
|
Assert.Equal(mockInvoice.AmountDue, invoice.AmountDue);
|
||||||
|
|
||||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<InvoiceGetOptions>(),
|
Arg.Any<InvoiceGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetInvoice_Fresh_Expand_ReturnsAPIInvoice()
|
public async Task GetInvoice_Fresh_Expand_ReturnsAPIInvoice()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
|
var eventInvoice = new Invoice { Id = "in_test", AmountDue = 1000 };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", eventInvoice);
|
||||||
|
|
||||||
var eventInvoice = stripeEvent.Data.Object as Invoice;
|
var apiInvoice = new Invoice { Id = "in_test", AmountDue = 2000 };
|
||||||
|
|
||||||
var apiInvoice = Copy(eventInvoice);
|
|
||||||
|
|
||||||
var expand = new List<string> { "customer" };
|
var expand = new List<string> { "customer" };
|
||||||
|
|
||||||
@@ -234,9 +219,7 @@ public class StripeEventServiceTests
|
|||||||
|
|
||||||
await _stripeFacade.Received().GetInvoice(
|
await _stripeFacade.Received().GetInvoice(
|
||||||
apiInvoice.Id,
|
apiInvoice.Id,
|
||||||
Arg.Is<InvoiceGetOptions>(options => options.Expand == expand),
|
Arg.Is<InvoiceGetOptions>(options => options.Expand == expand));
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -245,50 +228,44 @@ public class StripeEventServiceTests
|
|||||||
public async Task GetPaymentMethod_EventNotPaymentMethodRelated_ThrowsException()
|
public async Task GetPaymentMethod_EventNotPaymentMethodRelated_ThrowsException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
|
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
|
||||||
|
|
||||||
// Act
|
// Act & Assert
|
||||||
var function = async () => await _stripeEventService.GetPaymentMethod(stripeEvent);
|
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetPaymentMethod(stripeEvent));
|
||||||
|
|
||||||
// Assert
|
|
||||||
var exception = await Assert.ThrowsAsync<Exception>(function);
|
|
||||||
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'", exception.Message);
|
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'", exception.Message);
|
||||||
|
|
||||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<PaymentMethodGetOptions>(),
|
Arg.Any<PaymentMethodGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetPaymentMethod_NotFresh_ReturnsEventPaymentMethod()
|
public async Task GetPaymentMethod_NotFresh_ReturnsEventPaymentMethod()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);
|
var mockPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", mockPaymentMethod);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent);
|
var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equivalent(stripeEvent.Data.Object as PaymentMethod, paymentMethod, true);
|
Assert.Equal(mockPaymentMethod.Id, paymentMethod.Id);
|
||||||
|
Assert.Equal(mockPaymentMethod.Type, paymentMethod.Type);
|
||||||
|
|
||||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<PaymentMethodGetOptions>(),
|
Arg.Any<PaymentMethodGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetPaymentMethod_Fresh_Expand_ReturnsAPIPaymentMethod()
|
public async Task GetPaymentMethod_Fresh_Expand_ReturnsAPIPaymentMethod()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);
|
var eventPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", eventPaymentMethod);
|
||||||
|
|
||||||
var eventPaymentMethod = stripeEvent.Data.Object as PaymentMethod;
|
var apiPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" };
|
||||||
|
|
||||||
var apiPaymentMethod = Copy(eventPaymentMethod);
|
|
||||||
|
|
||||||
var expand = new List<string> { "customer" };
|
var expand = new List<string> { "customer" };
|
||||||
|
|
||||||
@@ -306,9 +283,7 @@ public class StripeEventServiceTests
|
|||||||
|
|
||||||
await _stripeFacade.Received().GetPaymentMethod(
|
await _stripeFacade.Received().GetPaymentMethod(
|
||||||
apiPaymentMethod.Id,
|
apiPaymentMethod.Id,
|
||||||
Arg.Is<PaymentMethodGetOptions>(options => options.Expand == expand),
|
Arg.Is<PaymentMethodGetOptions>(options => options.Expand == expand));
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -317,50 +292,44 @@ public class StripeEventServiceTests
|
|||||||
public async Task GetSubscription_EventNotSubscriptionRelated_ThrowsException()
|
public async Task GetSubscription_EventNotSubscriptionRelated_ThrowsException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
|
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
|
||||||
|
|
||||||
// Act
|
// Act & Assert
|
||||||
var function = async () => await _stripeEventService.GetSubscription(stripeEvent);
|
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetSubscription(stripeEvent));
|
||||||
|
|
||||||
// Assert
|
|
||||||
var exception = await Assert.ThrowsAsync<Exception>(function);
|
|
||||||
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'", exception.Message);
|
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'", exception.Message);
|
||||||
|
|
||||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<SubscriptionGetOptions>(),
|
Arg.Any<SubscriptionGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetSubscription_NotFresh_ReturnsEventSubscription()
|
public async Task GetSubscription_NotFresh_ReturnsEventSubscription()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
|
var mockSubscription = new Subscription { Id = "sub_test", Status = "active" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var subscription = await _stripeEventService.GetSubscription(stripeEvent);
|
var subscription = await _stripeEventService.GetSubscription(stripeEvent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equivalent(stripeEvent.Data.Object as Subscription, subscription, true);
|
Assert.Equal(mockSubscription.Id, subscription.Id);
|
||||||
|
Assert.Equal(mockSubscription.Status, subscription.Status);
|
||||||
|
|
||||||
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<SubscriptionGetOptions>(),
|
Arg.Any<SubscriptionGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetSubscription_Fresh_Expand_ReturnsAPISubscription()
|
public async Task GetSubscription_Fresh_Expand_ReturnsAPISubscription()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
|
var eventSubscription = new Subscription { Id = "sub_test", Status = "active" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", eventSubscription);
|
||||||
|
|
||||||
var eventSubscription = stripeEvent.Data.Object as Subscription;
|
var apiSubscription = new Subscription { Id = "sub_test", Status = "canceled" };
|
||||||
|
|
||||||
var apiSubscription = Copy(eventSubscription);
|
|
||||||
|
|
||||||
var expand = new List<string> { "customer" };
|
var expand = new List<string> { "customer" };
|
||||||
|
|
||||||
@@ -378,9 +347,71 @@ public class StripeEventServiceTests
|
|||||||
|
|
||||||
await _stripeFacade.Received().GetSubscription(
|
await _stripeFacade.Received().GetSubscription(
|
||||||
apiSubscription.Id,
|
apiSubscription.Id,
|
||||||
Arg.Is<SubscriptionGetOptions>(options => options.Expand == expand),
|
Arg.Is<SubscriptionGetOptions>(options => options.Expand == expand));
|
||||||
Arg.Any<RequestOptions>(),
|
}
|
||||||
Arg.Any<CancellationToken>());
|
#endregion
|
||||||
|
|
||||||
|
#region GetSetupIntent
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSetupIntent_EventNotSetupIntentRelated_ThrowsException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" });
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetSetupIntent(stripeEvent));
|
||||||
|
Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(SetupIntent)}'", exception.Message);
|
||||||
|
|
||||||
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent(
|
||||||
|
Arg.Any<string>(),
|
||||||
|
Arg.Any<SetupIntentGetOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSetupIntent_NotFresh_ReturnsEventSetupIntent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockSetupIntent = new SetupIntent { Id = "seti_test", Status = "succeeded" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var setupIntent = await _stripeEventService.GetSetupIntent(stripeEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(mockSetupIntent.Id, setupIntent.Id);
|
||||||
|
Assert.Equal(mockSetupIntent.Status, setupIntent.Status);
|
||||||
|
|
||||||
|
await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent(
|
||||||
|
Arg.Any<string>(),
|
||||||
|
Arg.Any<SetupIntentGetOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSetupIntent_Fresh_Expand_ReturnsAPISetupIntent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var eventSetupIntent = new SetupIntent { Id = "seti_test", Status = "succeeded" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", eventSetupIntent);
|
||||||
|
|
||||||
|
var apiSetupIntent = new SetupIntent { Id = "seti_test", Status = "requires_action" };
|
||||||
|
|
||||||
|
var expand = new List<string> { "customer" };
|
||||||
|
|
||||||
|
_stripeFacade.GetSetupIntent(
|
||||||
|
apiSetupIntent.Id,
|
||||||
|
Arg.Is<SetupIntentGetOptions>(options => options.Expand == expand))
|
||||||
|
.Returns(apiSetupIntent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var setupIntent = await _stripeEventService.GetSetupIntent(stripeEvent, true, expand);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(apiSetupIntent, setupIntent);
|
||||||
|
Assert.NotSame(eventSetupIntent, setupIntent);
|
||||||
|
|
||||||
|
await _stripeFacade.Received().GetSetupIntent(
|
||||||
|
apiSetupIntent.Id,
|
||||||
|
Arg.Is<SetupIntentGetOptions>(options => options.Expand == expand));
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -389,18 +420,16 @@ public class StripeEventServiceTests
|
|||||||
public async Task ValidateCloudRegion_SubscriptionUpdated_Success()
|
public async Task ValidateCloudRegion_SubscriptionUpdated_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
|
var mockSubscription = new Subscription { Id = "sub_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
|
||||||
|
|
||||||
var subscription = Copy(stripeEvent.Data.Object as Subscription);
|
var customer = CreateMockCustomer();
|
||||||
|
mockSubscription.Customer = customer;
|
||||||
var customer = await GetCustomerAsync();
|
|
||||||
|
|
||||||
subscription.Customer = customer;
|
|
||||||
|
|
||||||
_stripeFacade.GetSubscription(
|
_stripeFacade.GetSubscription(
|
||||||
subscription.Id,
|
mockSubscription.Id,
|
||||||
Arg.Any<SubscriptionGetOptions>())
|
Arg.Any<SubscriptionGetOptions>())
|
||||||
.Returns(subscription);
|
.Returns(mockSubscription);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
@@ -409,28 +438,24 @@ public class StripeEventServiceTests
|
|||||||
Assert.True(cloudRegionValid);
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
await _stripeFacade.Received(1).GetSubscription(
|
await _stripeFacade.Received(1).GetSubscription(
|
||||||
subscription.Id,
|
mockSubscription.Id,
|
||||||
Arg.Any<SubscriptionGetOptions>(),
|
Arg.Any<SubscriptionGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateCloudRegion_ChargeSucceeded_Success()
|
public async Task ValidateCloudRegion_ChargeSucceeded_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);
|
var mockCharge = new Charge { Id = "ch_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", mockCharge);
|
||||||
|
|
||||||
var charge = Copy(stripeEvent.Data.Object as Charge);
|
var customer = CreateMockCustomer();
|
||||||
|
mockCharge.Customer = customer;
|
||||||
var customer = await GetCustomerAsync();
|
|
||||||
|
|
||||||
charge.Customer = customer;
|
|
||||||
|
|
||||||
_stripeFacade.GetCharge(
|
_stripeFacade.GetCharge(
|
||||||
charge.Id,
|
mockCharge.Id,
|
||||||
Arg.Any<ChargeGetOptions>())
|
Arg.Any<ChargeGetOptions>())
|
||||||
.Returns(charge);
|
.Returns(mockCharge);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
@@ -439,24 +464,21 @@ public class StripeEventServiceTests
|
|||||||
Assert.True(cloudRegionValid);
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
await _stripeFacade.Received(1).GetCharge(
|
await _stripeFacade.Received(1).GetCharge(
|
||||||
charge.Id,
|
mockCharge.Id,
|
||||||
Arg.Any<ChargeGetOptions>(),
|
Arg.Any<ChargeGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateCloudRegion_UpcomingInvoice_Success()
|
public async Task ValidateCloudRegion_UpcomingInvoice_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceUpcoming);
|
var mockInvoice = new Invoice { Id = "in_test", CustomerId = "cus_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "invoice.upcoming", mockInvoice);
|
||||||
|
|
||||||
var invoice = Copy(stripeEvent.Data.Object as Invoice);
|
var customer = CreateMockCustomer();
|
||||||
|
|
||||||
var customer = await GetCustomerAsync();
|
|
||||||
|
|
||||||
_stripeFacade.GetCustomer(
|
_stripeFacade.GetCustomer(
|
||||||
invoice.CustomerId,
|
mockInvoice.CustomerId,
|
||||||
Arg.Any<CustomerGetOptions>())
|
Arg.Any<CustomerGetOptions>())
|
||||||
.Returns(customer);
|
.Returns(customer);
|
||||||
|
|
||||||
@@ -467,28 +489,24 @@ public class StripeEventServiceTests
|
|||||||
Assert.True(cloudRegionValid);
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
await _stripeFacade.Received(1).GetCustomer(
|
await _stripeFacade.Received(1).GetCustomer(
|
||||||
invoice.CustomerId,
|
mockInvoice.CustomerId,
|
||||||
Arg.Any<CustomerGetOptions>(),
|
Arg.Any<CustomerGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateCloudRegion_InvoiceCreated_Success()
|
public async Task ValidateCloudRegion_InvoiceCreated_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);
|
var mockInvoice = new Invoice { Id = "in_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "invoice.created", mockInvoice);
|
||||||
|
|
||||||
var invoice = Copy(stripeEvent.Data.Object as Invoice);
|
var customer = CreateMockCustomer();
|
||||||
|
mockInvoice.Customer = customer;
|
||||||
var customer = await GetCustomerAsync();
|
|
||||||
|
|
||||||
invoice.Customer = customer;
|
|
||||||
|
|
||||||
_stripeFacade.GetInvoice(
|
_stripeFacade.GetInvoice(
|
||||||
invoice.Id,
|
mockInvoice.Id,
|
||||||
Arg.Any<InvoiceGetOptions>())
|
Arg.Any<InvoiceGetOptions>())
|
||||||
.Returns(invoice);
|
.Returns(mockInvoice);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
@@ -497,28 +515,24 @@ public class StripeEventServiceTests
|
|||||||
Assert.True(cloudRegionValid);
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
await _stripeFacade.Received(1).GetInvoice(
|
await _stripeFacade.Received(1).GetInvoice(
|
||||||
invoice.Id,
|
mockInvoice.Id,
|
||||||
Arg.Any<InvoiceGetOptions>(),
|
Arg.Any<InvoiceGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateCloudRegion_PaymentMethodAttached_Success()
|
public async Task ValidateCloudRegion_PaymentMethodAttached_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);
|
var mockPaymentMethod = new PaymentMethod { Id = "pm_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", mockPaymentMethod);
|
||||||
|
|
||||||
var paymentMethod = Copy(stripeEvent.Data.Object as PaymentMethod);
|
var customer = CreateMockCustomer();
|
||||||
|
mockPaymentMethod.Customer = customer;
|
||||||
var customer = await GetCustomerAsync();
|
|
||||||
|
|
||||||
paymentMethod.Customer = customer;
|
|
||||||
|
|
||||||
_stripeFacade.GetPaymentMethod(
|
_stripeFacade.GetPaymentMethod(
|
||||||
paymentMethod.Id,
|
mockPaymentMethod.Id,
|
||||||
Arg.Any<PaymentMethodGetOptions>())
|
Arg.Any<PaymentMethodGetOptions>())
|
||||||
.Returns(paymentMethod);
|
.Returns(mockPaymentMethod);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
@@ -527,24 +541,21 @@ public class StripeEventServiceTests
|
|||||||
Assert.True(cloudRegionValid);
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
await _stripeFacade.Received(1).GetPaymentMethod(
|
await _stripeFacade.Received(1).GetPaymentMethod(
|
||||||
paymentMethod.Id,
|
mockPaymentMethod.Id,
|
||||||
Arg.Any<PaymentMethodGetOptions>(),
|
Arg.Any<PaymentMethodGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateCloudRegion_CustomerUpdated_Success()
|
public async Task ValidateCloudRegion_CustomerUpdated_Success()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);
|
var mockCustomer = CreateMockCustomer();
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "customer.updated", mockCustomer);
|
||||||
var customer = Copy(stripeEvent.Data.Object as Customer);
|
|
||||||
|
|
||||||
_stripeFacade.GetCustomer(
|
_stripeFacade.GetCustomer(
|
||||||
customer.Id,
|
mockCustomer.Id,
|
||||||
Arg.Any<CustomerGetOptions>())
|
Arg.Any<CustomerGetOptions>())
|
||||||
.Returns(customer);
|
.Returns(mockCustomer);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
@@ -553,29 +564,24 @@ public class StripeEventServiceTests
|
|||||||
Assert.True(cloudRegionValid);
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
await _stripeFacade.Received(1).GetCustomer(
|
await _stripeFacade.Received(1).GetCustomer(
|
||||||
customer.Id,
|
mockCustomer.Id,
|
||||||
Arg.Any<CustomerGetOptions>(),
|
Arg.Any<CustomerGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateCloudRegion_MetadataNull_ReturnsFalse()
|
public async Task ValidateCloudRegion_MetadataNull_ReturnsFalse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
|
var mockSubscription = new Subscription { Id = "sub_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
|
||||||
|
|
||||||
var subscription = Copy(stripeEvent.Data.Object as Subscription);
|
var customer = new Customer { Id = "cus_test", Metadata = null };
|
||||||
|
mockSubscription.Customer = customer;
|
||||||
var customer = await GetCustomerAsync();
|
|
||||||
customer.Metadata = null;
|
|
||||||
|
|
||||||
subscription.Customer = customer;
|
|
||||||
|
|
||||||
_stripeFacade.GetSubscription(
|
_stripeFacade.GetSubscription(
|
||||||
subscription.Id,
|
mockSubscription.Id,
|
||||||
Arg.Any<SubscriptionGetOptions>())
|
Arg.Any<SubscriptionGetOptions>())
|
||||||
.Returns(subscription);
|
.Returns(mockSubscription);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
@@ -584,29 +590,24 @@ public class StripeEventServiceTests
|
|||||||
Assert.False(cloudRegionValid);
|
Assert.False(cloudRegionValid);
|
||||||
|
|
||||||
await _stripeFacade.Received(1).GetSubscription(
|
await _stripeFacade.Received(1).GetSubscription(
|
||||||
subscription.Id,
|
mockSubscription.Id,
|
||||||
Arg.Any<SubscriptionGetOptions>(),
|
Arg.Any<SubscriptionGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateCloudRegion_MetadataNoRegion_DefaultUS_ReturnsTrue()
|
public async Task ValidateCloudRegion_MetadataNoRegion_DefaultUS_ReturnsTrue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
|
var mockSubscription = new Subscription { Id = "sub_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
|
||||||
|
|
||||||
var subscription = Copy(stripeEvent.Data.Object as Subscription);
|
var customer = new Customer { Id = "cus_test", Metadata = new Dictionary<string, string>() };
|
||||||
|
mockSubscription.Customer = customer;
|
||||||
var customer = await GetCustomerAsync();
|
|
||||||
customer.Metadata = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
subscription.Customer = customer;
|
|
||||||
|
|
||||||
_stripeFacade.GetSubscription(
|
_stripeFacade.GetSubscription(
|
||||||
subscription.Id,
|
mockSubscription.Id,
|
||||||
Arg.Any<SubscriptionGetOptions>())
|
Arg.Any<SubscriptionGetOptions>())
|
||||||
.Returns(subscription);
|
.Returns(mockSubscription);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
@@ -615,32 +616,28 @@ public class StripeEventServiceTests
|
|||||||
Assert.True(cloudRegionValid);
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
await _stripeFacade.Received(1).GetSubscription(
|
await _stripeFacade.Received(1).GetSubscription(
|
||||||
subscription.Id,
|
mockSubscription.Id,
|
||||||
Arg.Any<SubscriptionGetOptions>(),
|
Arg.Any<SubscriptionGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateCloudRegion_MetadataMiscasedRegion_ReturnsTrue()
|
public async Task ValidateCloudRegion_MetadataIncorrectlyCasedRegion_ReturnsTrue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);
|
var mockSubscription = new Subscription { Id = "sub_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription);
|
||||||
|
|
||||||
var subscription = Copy(stripeEvent.Data.Object as Subscription);
|
var customer = new Customer
|
||||||
|
|
||||||
var customer = await GetCustomerAsync();
|
|
||||||
customer.Metadata = new Dictionary<string, string>
|
|
||||||
{
|
{
|
||||||
{ "Region", "US" }
|
Id = "cus_test",
|
||||||
|
Metadata = new Dictionary<string, string> { { "Region", "US" } }
|
||||||
};
|
};
|
||||||
|
mockSubscription.Customer = customer;
|
||||||
subscription.Customer = customer;
|
|
||||||
|
|
||||||
_stripeFacade.GetSubscription(
|
_stripeFacade.GetSubscription(
|
||||||
subscription.Id,
|
mockSubscription.Id,
|
||||||
Arg.Any<SubscriptionGetOptions>())
|
Arg.Any<SubscriptionGetOptions>())
|
||||||
.Returns(subscription);
|
.Returns(mockSubscription);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
@@ -649,31 +646,209 @@ public class StripeEventServiceTests
|
|||||||
Assert.True(cloudRegionValid);
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
await _stripeFacade.Received(1).GetSubscription(
|
await _stripeFacade.Received(1).GetSubscription(
|
||||||
subscription.Id,
|
mockSubscription.Id,
|
||||||
Arg.Any<SubscriptionGetOptions>(),
|
Arg.Any<SubscriptionGetOptions>());
|
||||||
Arg.Any<RequestOptions>(),
|
}
|
||||||
Arg.Any<CancellationToken>());
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidateCloudRegion_SetupIntentSucceeded_OrganizationCustomer_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
var organizationCustomerId = "cus_org_test";
|
||||||
|
|
||||||
|
var mockOrganization = new Core.AdminConsole.Entities.Organization
|
||||||
|
{
|
||||||
|
Id = organizationId,
|
||||||
|
GatewayCustomerId = organizationCustomerId
|
||||||
|
};
|
||||||
|
var customer = CreateMockCustomer();
|
||||||
|
|
||||||
|
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
|
||||||
|
.Returns(organizationId);
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(organizationId)
|
||||||
|
.Returns(mockOrganization);
|
||||||
|
|
||||||
|
_stripeFacade.GetCustomer(organizationCustomerId)
|
||||||
|
.Returns(customer);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
|
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
|
||||||
|
await _organizationRepository.Received(1).GetByIdAsync(organizationId);
|
||||||
|
await _stripeFacade.Received(1).GetCustomer(organizationCustomerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidateCloudRegion_SetupIntentSucceeded_ProviderCustomer_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||||
|
var providerId = Guid.NewGuid();
|
||||||
|
var providerCustomerId = "cus_provider_test";
|
||||||
|
|
||||||
|
var mockProvider = new Core.AdminConsole.Entities.Provider.Provider
|
||||||
|
{
|
||||||
|
Id = providerId,
|
||||||
|
GatewayCustomerId = providerCustomerId
|
||||||
|
};
|
||||||
|
var customer = CreateMockCustomer();
|
||||||
|
|
||||||
|
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
|
||||||
|
.Returns(providerId);
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(providerId)
|
||||||
|
.Returns((Core.AdminConsole.Entities.Organization?)null);
|
||||||
|
|
||||||
|
_providerRepository.GetByIdAsync(providerId)
|
||||||
|
.Returns(mockProvider);
|
||||||
|
|
||||||
|
_stripeFacade.GetCustomer(providerCustomerId)
|
||||||
|
.Returns(customer);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
|
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
|
||||||
|
await _organizationRepository.Received(1).GetByIdAsync(providerId);
|
||||||
|
await _providerRepository.Received(1).GetByIdAsync(providerId);
|
||||||
|
await _stripeFacade.Received(1).GetCustomer(providerCustomerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidateCloudRegion_SetupIntentSucceeded_NoSubscriberIdInCache_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||||
|
|
||||||
|
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
|
||||||
|
.Returns((Guid?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(cloudRegionValid);
|
||||||
|
|
||||||
|
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
|
||||||
|
await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||||
|
await _providerRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||||
|
await _stripeFacade.DidNotReceive().GetCustomer(Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidateCloudRegion_SetupIntentSucceeded_OrganizationWithoutGatewayCustomerId_ChecksProvider()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||||
|
var subscriberId = Guid.NewGuid();
|
||||||
|
var providerCustomerId = "cus_provider_test";
|
||||||
|
|
||||||
|
var mockOrganizationWithoutCustomerId = new Core.AdminConsole.Entities.Organization
|
||||||
|
{
|
||||||
|
Id = subscriberId,
|
||||||
|
GatewayCustomerId = null
|
||||||
|
};
|
||||||
|
|
||||||
|
var mockProvider = new Core.AdminConsole.Entities.Provider.Provider
|
||||||
|
{
|
||||||
|
Id = subscriberId,
|
||||||
|
GatewayCustomerId = providerCustomerId
|
||||||
|
};
|
||||||
|
var customer = CreateMockCustomer();
|
||||||
|
|
||||||
|
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
|
||||||
|
.Returns(subscriberId);
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(subscriberId)
|
||||||
|
.Returns(mockOrganizationWithoutCustomerId);
|
||||||
|
|
||||||
|
_providerRepository.GetByIdAsync(subscriberId)
|
||||||
|
.Returns(mockProvider);
|
||||||
|
|
||||||
|
_stripeFacade.GetCustomer(providerCustomerId)
|
||||||
|
.Returns(customer);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(cloudRegionValid);
|
||||||
|
|
||||||
|
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
|
||||||
|
await _organizationRepository.Received(1).GetByIdAsync(subscriberId);
|
||||||
|
await _providerRepository.Received(1).GetByIdAsync(subscriberId);
|
||||||
|
await _stripeFacade.Received(1).GetCustomer(providerCustomerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidateCloudRegion_SetupIntentSucceeded_ProviderWithoutGatewayCustomerId_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockSetupIntent = new SetupIntent { Id = "seti_test" };
|
||||||
|
var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent);
|
||||||
|
var subscriberId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var mockProviderWithoutCustomerId = new Core.AdminConsole.Entities.Provider.Provider
|
||||||
|
{
|
||||||
|
Id = subscriberId,
|
||||||
|
GatewayCustomerId = null
|
||||||
|
};
|
||||||
|
|
||||||
|
_setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id)
|
||||||
|
.Returns(subscriberId);
|
||||||
|
|
||||||
|
_organizationRepository.GetByIdAsync(subscriberId)
|
||||||
|
.Returns((Core.AdminConsole.Entities.Organization?)null);
|
||||||
|
|
||||||
|
_providerRepository.GetByIdAsync(subscriberId)
|
||||||
|
.Returns(mockProviderWithoutCustomerId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(cloudRegionValid);
|
||||||
|
|
||||||
|
await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id);
|
||||||
|
await _organizationRepository.Received(1).GetByIdAsync(subscriberId);
|
||||||
|
await _providerRepository.Received(1).GetByIdAsync(subscriberId);
|
||||||
|
await _stripeFacade.DidNotReceive().GetCustomer(Arg.Any<string>());
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private static T Copy<T>(T input)
|
private static Event CreateMockEvent<T>(string id, string type, T dataObject) where T : IStripeEntity
|
||||||
{
|
{
|
||||||
var copy = (T)Activator.CreateInstance(typeof(T));
|
return new Event
|
||||||
|
|
||||||
var properties = input.GetType().GetProperties();
|
|
||||||
|
|
||||||
foreach (var property in properties)
|
|
||||||
{
|
{
|
||||||
var value = property.GetValue(input);
|
Id = id,
|
||||||
copy!
|
Type = type,
|
||||||
.GetType()
|
Data = new EventData
|
||||||
.GetProperty(property.Name)!
|
{
|
||||||
.SetValue(copy, value);
|
Object = (IHasObject)dataObject
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return copy;
|
private static Customer CreateMockCustomer()
|
||||||
|
{
|
||||||
|
return new Customer
|
||||||
|
{
|
||||||
|
Id = "cus_test",
|
||||||
|
Metadata = new Dictionary<string, string> { { "region", "US" } }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<Customer> GetCustomerAsync()
|
|
||||||
=> (await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated)).Data.Object as Customer;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Platform.Push;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -33,7 +32,6 @@ public class SubscriptionUpdatedHandlerTests
|
|||||||
private readonly IStripeFacade _stripeFacade;
|
private readonly IStripeFacade _stripeFacade;
|
||||||
private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand;
|
private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationEnableCommand _organizationEnableCommand;
|
private readonly IOrganizationEnableCommand _organizationEnableCommand;
|
||||||
private readonly IOrganizationDisableCommand _organizationDisableCommand;
|
private readonly IOrganizationDisableCommand _organizationDisableCommand;
|
||||||
@@ -42,6 +40,7 @@ public class SubscriptionUpdatedHandlerTests
|
|||||||
private readonly IProviderRepository _providerRepository;
|
private readonly IProviderRepository _providerRepository;
|
||||||
private readonly IProviderService _providerService;
|
private readonly IProviderService _providerService;
|
||||||
private readonly IScheduler _scheduler;
|
private readonly IScheduler _scheduler;
|
||||||
|
private readonly IPushNotificationAdapter _pushNotificationAdapter;
|
||||||
private readonly SubscriptionUpdatedHandler _sut;
|
private readonly SubscriptionUpdatedHandler _sut;
|
||||||
|
|
||||||
public SubscriptionUpdatedHandlerTests()
|
public SubscriptionUpdatedHandlerTests()
|
||||||
@@ -53,7 +52,6 @@ public class SubscriptionUpdatedHandlerTests
|
|||||||
_organizationSponsorshipRenewCommand = Substitute.For<IOrganizationSponsorshipRenewCommand>();
|
_organizationSponsorshipRenewCommand = Substitute.For<IOrganizationSponsorshipRenewCommand>();
|
||||||
_userService = Substitute.For<IUserService>();
|
_userService = Substitute.For<IUserService>();
|
||||||
_providerService = Substitute.For<IProviderService>();
|
_providerService = Substitute.For<IProviderService>();
|
||||||
_pushNotificationService = Substitute.For<IPushNotificationService>();
|
|
||||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
var schedulerFactory = Substitute.For<ISchedulerFactory>();
|
var schedulerFactory = Substitute.For<ISchedulerFactory>();
|
||||||
_organizationEnableCommand = Substitute.For<IOrganizationEnableCommand>();
|
_organizationEnableCommand = Substitute.For<IOrganizationEnableCommand>();
|
||||||
@@ -64,6 +62,7 @@ public class SubscriptionUpdatedHandlerTests
|
|||||||
_providerService = Substitute.For<IProviderService>();
|
_providerService = Substitute.For<IProviderService>();
|
||||||
var logger = Substitute.For<ILogger<SubscriptionUpdatedHandler>>();
|
var logger = Substitute.For<ILogger<SubscriptionUpdatedHandler>>();
|
||||||
_scheduler = Substitute.For<IScheduler>();
|
_scheduler = Substitute.For<IScheduler>();
|
||||||
|
_pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();
|
||||||
|
|
||||||
schedulerFactory.GetScheduler().Returns(_scheduler);
|
schedulerFactory.GetScheduler().Returns(_scheduler);
|
||||||
|
|
||||||
@@ -74,7 +73,6 @@ public class SubscriptionUpdatedHandlerTests
|
|||||||
_stripeFacade,
|
_stripeFacade,
|
||||||
_organizationSponsorshipRenewCommand,
|
_organizationSponsorshipRenewCommand,
|
||||||
_userService,
|
_userService,
|
||||||
_pushNotificationService,
|
|
||||||
_organizationRepository,
|
_organizationRepository,
|
||||||
schedulerFactory,
|
schedulerFactory,
|
||||||
_organizationEnableCommand,
|
_organizationEnableCommand,
|
||||||
@@ -83,7 +81,8 @@ public class SubscriptionUpdatedHandlerTests
|
|||||||
_featureService,
|
_featureService,
|
||||||
_providerRepository,
|
_providerRepository,
|
||||||
_providerService,
|
_providerService,
|
||||||
logger);
|
logger,
|
||||||
|
_pushNotificationAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -540,8 +539,8 @@ public class SubscriptionUpdatedHandlerTests
|
|||||||
.EnableAsync(organizationId);
|
.EnableAsync(organizationId);
|
||||||
await _organizationService.Received(1)
|
await _organizationService.Received(1)
|
||||||
.UpdateExpirationDateAsync(organizationId, currentPeriodEnd);
|
.UpdateExpirationDateAsync(organizationId, currentPeriodEnd);
|
||||||
await _pushNotificationService.Received(1)
|
await _pushNotificationAdapter.Received(1)
|
||||||
.PushSyncOrganizationStatusAsync(organization);
|
.NotifyEnabledChangedAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public class GetOrganizationWarningsQueryTests
|
|||||||
});
|
});
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
|
||||||
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns((string?)null);
|
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(organization.Id).Returns((string?)null);
|
||||||
|
|
||||||
var response = await sutProvider.Sut.Run(organization);
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ public class GetOrganizationWarningsQueryTests
|
|||||||
});
|
});
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
|
||||||
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns(setupIntentId);
|
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(organization.Id).Returns(setupIntentId);
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(
|
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(
|
||||||
options => options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
options => options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ public class UpdatePaymentMethodCommandTests
|
|||||||
},
|
},
|
||||||
NextAction = new SetupIntentNextAction
|
NextAction = new SetupIntentNextAction
|
||||||
{
|
{
|
||||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
|
||||||
|
{
|
||||||
|
HostedVerificationUrl = "https://example.com"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Status = "requires_action"
|
Status = "requires_action"
|
||||||
};
|
};
|
||||||
@@ -95,7 +98,7 @@ public class UpdatePaymentMethodCommandTests
|
|||||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||||
Assert.False(maskedBankAccount.Verified);
|
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
|
||||||
|
|
||||||
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
|
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
|
||||||
}
|
}
|
||||||
@@ -133,7 +136,10 @@ public class UpdatePaymentMethodCommandTests
|
|||||||
},
|
},
|
||||||
NextAction = new SetupIntentNextAction
|
NextAction = new SetupIntentNextAction
|
||||||
{
|
{
|
||||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
|
||||||
|
{
|
||||||
|
HostedVerificationUrl = "https://example.com"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Status = "requires_action"
|
Status = "requires_action"
|
||||||
};
|
};
|
||||||
@@ -154,7 +160,7 @@ public class UpdatePaymentMethodCommandTests
|
|||||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||||
Assert.False(maskedBankAccount.Verified);
|
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
|
||||||
|
|
||||||
await _subscriberService.Received(1).CreateStripeCustomer(organization);
|
await _subscriberService.Received(1).CreateStripeCustomer(organization);
|
||||||
|
|
||||||
@@ -199,7 +205,10 @@ public class UpdatePaymentMethodCommandTests
|
|||||||
},
|
},
|
||||||
NextAction = new SetupIntentNextAction
|
NextAction = new SetupIntentNextAction
|
||||||
{
|
{
|
||||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
|
||||||
|
{
|
||||||
|
HostedVerificationUrl = "https://example.com"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Status = "requires_action"
|
Status = "requires_action"
|
||||||
};
|
};
|
||||||
@@ -220,7 +229,7 @@ public class UpdatePaymentMethodCommandTests
|
|||||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||||
Assert.False(maskedBankAccount.Verified);
|
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
|
||||||
|
|
||||||
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
|
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
|
||||||
await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>
|
await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.Billing.Caches;
|
|
||||||
using Bit.Core.Billing.Payment.Commands;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Test.Billing.Extensions;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NSubstitute;
|
|
||||||
using Stripe;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Bit.Core.Test.Billing.Payment.Commands;
|
|
||||||
|
|
||||||
public class VerifyBankAccountCommandTests
|
|
||||||
{
|
|
||||||
private readonly ISetupIntentCache _setupIntentCache = Substitute.For<ISetupIntentCache>();
|
|
||||||
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
|
|
||||||
private readonly VerifyBankAccountCommand _command;
|
|
||||||
|
|
||||||
public VerifyBankAccountCommandTests()
|
|
||||||
{
|
|
||||||
_command = new VerifyBankAccountCommand(
|
|
||||||
Substitute.For<ILogger<VerifyBankAccountCommand>>(),
|
|
||||||
_setupIntentCache,
|
|
||||||
_stripeAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Run_MakesCorrectInvocations_ReturnsMaskedBankAccount()
|
|
||||||
{
|
|
||||||
var organization = new Organization
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
GatewayCustomerId = "cus_123"
|
|
||||||
};
|
|
||||||
|
|
||||||
const string setupIntentId = "seti_123";
|
|
||||||
|
|
||||||
_setupIntentCache.Get(organization.Id).Returns(setupIntentId);
|
|
||||||
|
|
||||||
var setupIntent = new SetupIntent
|
|
||||||
{
|
|
||||||
Id = setupIntentId,
|
|
||||||
PaymentMethodId = "pm_123",
|
|
||||||
PaymentMethod =
|
|
||||||
new PaymentMethod
|
|
||||||
{
|
|
||||||
Id = "pm_123",
|
|
||||||
Type = "us_bank_account",
|
|
||||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
|
||||||
},
|
|
||||||
NextAction = new SetupIntentNextAction
|
|
||||||
{
|
|
||||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
|
||||||
},
|
|
||||||
Status = "requires_action"
|
|
||||||
};
|
|
||||||
|
|
||||||
_stripeAdapter.SetupIntentGet(setupIntentId,
|
|
||||||
Arg.Is<SetupIntentGetOptions>(options => options.HasExpansions("payment_method"))).Returns(setupIntent);
|
|
||||||
|
|
||||||
_stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
|
|
||||||
Arg.Is<PaymentMethodAttachOptions>(options => options.Customer == organization.GatewayCustomerId))
|
|
||||||
.Returns(setupIntent.PaymentMethod);
|
|
||||||
|
|
||||||
var result = await _command.Run(organization, "DESCRIPTOR_CODE");
|
|
||||||
|
|
||||||
Assert.True(result.IsT0);
|
|
||||||
var maskedPaymentMethod = result.AsT0;
|
|
||||||
Assert.True(maskedPaymentMethod.IsT0);
|
|
||||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
|
||||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
|
||||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
|
||||||
Assert.True(maskedBankAccount.Verified);
|
|
||||||
|
|
||||||
await _stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id,
|
|
||||||
Arg.Is<SetupIntentVerifyMicrodepositsOptions>(options => options.DescriptorCode == "DESCRIPTOR_CODE"));
|
|
||||||
|
|
||||||
await _stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
|
||||||
options => options.InvoiceSettings.DefaultPaymentMethod == setupIntent.PaymentMethodId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ public class MaskedPaymentMethodTests
|
|||||||
{
|
{
|
||||||
BankName = "Chase",
|
BankName = "Chase",
|
||||||
Last4 = "9999",
|
Last4 = "9999",
|
||||||
Verified = true
|
HostedVerificationUrl = "https://example.com"
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(input);
|
var json = JsonSerializer.Serialize(input);
|
||||||
@@ -32,7 +32,7 @@ public class MaskedPaymentMethodTests
|
|||||||
{
|
{
|
||||||
BankName = "Chase",
|
BankName = "Chase",
|
||||||
Last4 = "9999",
|
Last4 = "9999",
|
||||||
Verified = true
|
HostedVerificationUrl = "https://example.com"
|
||||||
};
|
};
|
||||||
|
|
||||||
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ public class GetPaymentMethodQueryTests
|
|||||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||||
Assert.True(maskedBankAccount.Verified);
|
Assert.Null(maskedBankAccount.HostedVerificationUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -142,7 +142,7 @@ public class GetPaymentMethodQueryTests
|
|||||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||||
Assert.True(maskedBankAccount.Verified);
|
Assert.Null(maskedBankAccount.HostedVerificationUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -163,7 +163,7 @@ public class GetPaymentMethodQueryTests
|
|||||||
Arg.Is<CustomerGetOptions>(options =>
|
Arg.Is<CustomerGetOptions>(options =>
|
||||||
options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer);
|
options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer);
|
||||||
|
|
||||||
_setupIntentCache.Get(organization.Id).Returns("seti_123");
|
_setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123");
|
||||||
|
|
||||||
_stripeAdapter
|
_stripeAdapter
|
||||||
.SetupIntentGet("seti_123",
|
.SetupIntentGet("seti_123",
|
||||||
@@ -177,7 +177,10 @@ public class GetPaymentMethodQueryTests
|
|||||||
},
|
},
|
||||||
NextAction = new SetupIntentNextAction
|
NextAction = new SetupIntentNextAction
|
||||||
{
|
{
|
||||||
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits
|
||||||
|
{
|
||||||
|
HostedVerificationUrl = "https://example.com"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Status = "requires_action"
|
Status = "requires_action"
|
||||||
});
|
});
|
||||||
@@ -189,7 +192,7 @@ public class GetPaymentMethodQueryTests
|
|||||||
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
var maskedBankAccount = maskedPaymentMethod.AsT0;
|
||||||
Assert.Equal("Chase", maskedBankAccount.BankName);
|
Assert.Equal("Chase", maskedBankAccount.BankName);
|
||||||
Assert.Equal("9999", maskedBankAccount.Last4);
|
Assert.Equal("9999", maskedBankAccount.Last4);
|
||||||
Assert.False(maskedBankAccount.Verified);
|
Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ public class SubscriberServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntent.Id);
|
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id,
|
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id,
|
||||||
Arg.Is<SetupIntentGetOptions>(options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
|
Arg.Is<SetupIntentGetOptions>(options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
|
||||||
@@ -1876,7 +1876,7 @@ public class SubscriberServiceTests
|
|||||||
PaymentMethodId = "payment_method_id"
|
PaymentMethodId = "payment_method_id"
|
||||||
};
|
};
|
||||||
|
|
||||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntent.Id);
|
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
|
||||||
|
|||||||
@@ -651,31 +651,6 @@ public class AzureQueuePushEngineTests
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse()
|
|
||||||
{
|
|
||||||
var organization = new Organization
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Enabled = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
var expectedPayload = new JsonObject
|
|
||||||
{
|
|
||||||
["Type"] = 18,
|
|
||||||
["Payload"] = new JsonObject
|
|
||||||
{
|
|
||||||
["OrganizationId"] = organization.Id,
|
|
||||||
["Enabled"] = organization.Enabled,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await VerifyNotificationAsync(
|
|
||||||
async sut => await sut.PushSyncOrganizationStatusAsync(organization),
|
|
||||||
expectedPayload
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()
|
public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -413,21 +413,6 @@ public abstract class PushTestBase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse()
|
|
||||||
{
|
|
||||||
var organization = new Organization
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Enabled = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await VerifyNotificationAsync(
|
|
||||||
async sut => await sut.PushSyncOrganizationStatusAsync(organization),
|
|
||||||
GetPushSyncOrganizationStatusResponsePayload(organization)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()
|
public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user