mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-24273] Milestone 2C (#6544)
* feat(billing): add mjml template and updated templates * feat(billing): update maileservices * feat(billing): add milestone2 discount * feat(billing): add milestone 2 updates and stripe constants * tests(billing): add handler tests * fix(billing): update mailer view and templates * fix(billing): revert mailservice changes * fix(billing): swap mailer service in handler * test(billing): update handler tests
This commit is contained in:
@@ -2,18 +2,22 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Mail.UpdatedInvoiceIncoming;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
using static Bit.Core.Billing.Constants.StripeConstants;
|
||||
using Event = Stripe.Event;
|
||||
|
||||
namespace Bit.Billing.Services.Implementations;
|
||||
@@ -29,7 +33,9 @@ public class UpcomingInvoiceHandler(
|
||||
IStripeEventService stripeEventService,
|
||||
IStripeEventUtilityService stripeEventUtilityService,
|
||||
IUserRepository userRepository,
|
||||
IValidateSponsorshipCommand validateSponsorshipCommand)
|
||||
IValidateSponsorshipCommand validateSponsorshipCommand,
|
||||
IMailer mailer,
|
||||
IFeatureService featureService)
|
||||
: IUpcomingInvoiceHandler
|
||||
{
|
||||
public async Task HandleAsync(Event parsedEvent)
|
||||
@@ -37,7 +43,8 @@ public class UpcomingInvoiceHandler(
|
||||
var invoice = await stripeEventService.GetInvoice(parsedEvent);
|
||||
|
||||
var customer =
|
||||
await stripeFacade.GetCustomer(invoice.CustomerId, new CustomerGetOptions { Expand = ["subscriptions", "tax", "tax_ids"] });
|
||||
await stripeFacade.GetCustomer(invoice.CustomerId,
|
||||
new CustomerGetOptions { Expand = ["subscriptions", "tax", "tax_ids"] });
|
||||
|
||||
var subscription = customer.Subscriptions.FirstOrDefault();
|
||||
|
||||
@@ -68,7 +75,8 @@ public class UpcomingInvoiceHandler(
|
||||
|
||||
if (stripeEventUtilityService.IsSponsoredSubscription(subscription))
|
||||
{
|
||||
var sponsorshipIsValid = await validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value);
|
||||
var sponsorshipIsValid =
|
||||
await validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value);
|
||||
|
||||
if (!sponsorshipIsValid)
|
||||
{
|
||||
@@ -122,9 +130,17 @@ public class UpcomingInvoiceHandler(
|
||||
}
|
||||
}
|
||||
|
||||
var milestone2Feature = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2);
|
||||
if (milestone2Feature)
|
||||
{
|
||||
await UpdateSubscriptionItemPriceIdAsync(parsedEvent, subscription, user);
|
||||
}
|
||||
|
||||
if (user.Premium)
|
||||
{
|
||||
await SendUpcomingInvoiceEmailsAsync(new List<string> { user.Email }, invoice);
|
||||
await (milestone2Feature
|
||||
? SendUpdatedUpcomingInvoiceEmailsAsync(new List<string> { user.Email })
|
||||
: SendUpcomingInvoiceEmailsAsync(new List<string> { user.Email }, invoice));
|
||||
}
|
||||
}
|
||||
else if (providerId.HasValue)
|
||||
@@ -142,6 +158,39 @@ public class UpcomingInvoiceHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateSubscriptionItemPriceIdAsync(Event parsedEvent, Subscription subscription, User user)
|
||||
{
|
||||
var pricingItem =
|
||||
subscription.Items.FirstOrDefault(i => i.Price.Id == Prices.PremiumAnnually);
|
||||
if (pricingItem != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plan = await pricingClient.GetAvailablePremiumPlan();
|
||||
await stripeFacade.UpdateSubscription(subscription.Id,
|
||||
new SubscriptionUpdateOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new SubscriptionItemOptions { Id = pricingItem.Id, Price = plan.Seat.StripePriceId }
|
||||
],
|
||||
Discounts =
|
||||
[
|
||||
new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone2SubscriptionDiscount }
|
||||
]
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to update user's ({UserID}) subscription price id while processing event with ID {EventID}",
|
||||
user.Id,
|
||||
parsedEvent.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendUpcomingInvoiceEmailsAsync(IEnumerable<string> emails, Invoice invoice)
|
||||
{
|
||||
var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));
|
||||
@@ -159,7 +208,19 @@ public class UpcomingInvoiceHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendProviderUpcomingInvoiceEmailsAsync(IEnumerable<string> emails, Invoice invoice, Subscription subscription, Guid providerId)
|
||||
private async Task SendUpdatedUpcomingInvoiceEmailsAsync(IEnumerable<string> emails)
|
||||
{
|
||||
var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));
|
||||
var updatedUpcomingEmail = new UpdatedInvoiceUpcomingMail
|
||||
{
|
||||
ToEmails = validEmails,
|
||||
View = new UpdatedInvoiceUpcomingView()
|
||||
};
|
||||
await mailer.SendEmail(updatedUpcomingEmail);
|
||||
}
|
||||
|
||||
private async Task SendProviderUpcomingInvoiceEmailsAsync(IEnumerable<string> emails, Invoice invoice,
|
||||
Subscription subscription, Guid providerId)
|
||||
{
|
||||
var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));
|
||||
|
||||
@@ -205,12 +266,12 @@ public class UpcomingInvoiceHandler(
|
||||
organization.PlanType.GetProductTier() != ProductTierType.Families &&
|
||||
customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates;
|
||||
|
||||
if (nonUSBusinessUse && customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||
if (nonUSBusinessUse && customer.TaxExempt != TaxExempt.Reverse)
|
||||
{
|
||||
try
|
||||
{
|
||||
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||
new CustomerUpdateOptions { TaxExempt = TaxExempt.Reverse });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -250,12 +311,12 @@ public class UpcomingInvoiceHandler(
|
||||
string eventId)
|
||||
{
|
||||
if (customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates &&
|
||||
customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||
customer.TaxExempt != TaxExempt.Reverse)
|
||||
{
|
||||
try
|
||||
{
|
||||
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||
new CustomerUpdateOptions { TaxExempt = TaxExempt.Reverse });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ public static class StripeConstants
|
||||
{
|
||||
public const string LegacyMSPDiscount = "msp-discount-35";
|
||||
public const string SecretsManagerStandalone = "sm-standalone";
|
||||
public const string Milestone2SubscriptionDiscount = "cm3nHfO1";
|
||||
|
||||
public static class MSPDiscounts
|
||||
{
|
||||
|
||||
27
src/Core/MailTemplates/Mjml/emails/invoice-upcoming.mjml
Normal file
27
src/Core/MailTemplates/Mjml/emails/invoice-upcoming.mjml
Normal file
@@ -0,0 +1,27 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-include path="../components/head.mjml" />
|
||||
</mj-head>
|
||||
|
||||
<mj-body background-color="#f6f6f6">
|
||||
<mj-include path="../components/logo.mjml" />
|
||||
|
||||
<mj-wrapper
|
||||
background-color="#fff"
|
||||
border="1px solid #e9e9e9"
|
||||
css-class="border-fix"
|
||||
padding="0"
|
||||
>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc semper sapien non sem tincidunt pretium ut vitae tortor. Mauris mattis id arcu in dictum. Vivamus tempor maximus elit id convallis. Pellentesque ligula nisl, bibendum eu maximus sit amet, rutrum efficitur tortor. Cras non dignissim leo, eget gravida odio. Nullam tincidunt porta fermentum. Fusce sit amet sagittis nunc.
|
||||
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<mj-include path="../components/footer.mjml" />
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -0,0 +1,10 @@
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
|
||||
namespace Bit.Core.Models.Mail.UpdatedInvoiceIncoming;
|
||||
|
||||
public class UpdatedInvoiceUpcomingView : BaseMailView;
|
||||
|
||||
public class UpdatedInvoiceUpcomingMail : BaseMail<UpdatedInvoiceUpcomingView>
|
||||
{
|
||||
public override string Subject { get => "Your Subscription Will Renew Soon"; }
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc semper sapien non sem tincidunt pretium ut vitae tortor. Mauris mattis id arcu in dictum. Vivamus tempor maximus elit id convallis. Pellentesque ligula nisl, bibendum eu maximus sit amet, rutrum efficitur tortor. Cras non dignissim leo, eget gravida odio. Nullam tincidunt porta fermentum. Fusce sit amet sagittis nunc.
|
||||
{{/BasicTextLayout}}
|
||||
947
test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs
Normal file
947
test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs
Normal file
@@ -0,0 +1,947 @@
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Billing.Services.Implementations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Pricing.Premium;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Mail.UpdatedInvoiceIncoming;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
using static Bit.Core.Billing.Constants.StripeConstants;
|
||||
using Address = Stripe.Address;
|
||||
using Event = Stripe.Event;
|
||||
using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;
|
||||
|
||||
namespace Bit.Billing.Test.Services;
|
||||
|
||||
public class UpcomingInvoiceHandlerTests
|
||||
{
|
||||
private readonly IGetPaymentMethodQuery _getPaymentMethodQuery;
|
||||
private readonly ILogger<StripeEventProcessor> _logger;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IStripeFacade _stripeFacade;
|
||||
private readonly IStripeEventService _stripeEventService;
|
||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IValidateSponsorshipCommand _validateSponsorshipCommand;
|
||||
private readonly IMailer _mailer;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private readonly UpcomingInvoiceHandler _sut;
|
||||
|
||||
private readonly Guid _userId = Guid.NewGuid();
|
||||
private readonly Guid _organizationId = Guid.NewGuid();
|
||||
private readonly Guid _providerId = Guid.NewGuid();
|
||||
|
||||
|
||||
public UpcomingInvoiceHandlerTests()
|
||||
{
|
||||
_getPaymentMethodQuery = Substitute.For<IGetPaymentMethodQuery>();
|
||||
_logger = Substitute.For<ILogger<StripeEventProcessor>>();
|
||||
_mailService = Substitute.For<IMailService>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_pricingClient = Substitute.For<IPricingClient>();
|
||||
_providerRepository = Substitute.For<IProviderRepository>();
|
||||
_stripeFacade = Substitute.For<IStripeFacade>();
|
||||
_stripeEventService = Substitute.For<IStripeEventService>();
|
||||
_stripeEventUtilityService = Substitute.For<IStripeEventUtilityService>();
|
||||
_userRepository = Substitute.For<IUserRepository>();
|
||||
_validateSponsorshipCommand = Substitute.For<IValidateSponsorshipCommand>();
|
||||
_mailer = Substitute.For<IMailer>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
|
||||
_sut = new UpcomingInvoiceHandler(
|
||||
_getPaymentMethodQuery,
|
||||
_logger,
|
||||
_mailService,
|
||||
_organizationRepository,
|
||||
_pricingClient,
|
||||
_providerRepository,
|
||||
_stripeFacade,
|
||||
_stripeEventService,
|
||||
_stripeEventUtilityService,
|
||||
_userRepository,
|
||||
_validateSponsorshipCommand,
|
||||
_mailer,
|
||||
_featureService);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenNullSubscription_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event();
|
||||
var invoice = new Invoice { CustomerId = "cus_123" };
|
||||
var customer = new Customer { Id = "cus_123", Subscriptions = new StripeList<Subscription> { Data = [] } };
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade
|
||||
.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(customer);
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.DidNotReceive()
|
||||
.UpdateCustomer(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenValidUser_SendsEmail()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var customerId = "cus_123";
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = customerId,
|
||||
AmountDue = 10000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new() { Id = "si_123", Price = new Price { Id = Prices.PremiumAnnually } }
|
||||
}
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = customerId },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
var user = new User { Id = _userId, Email = "user@example.com", Premium = true };
|
||||
var plan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new Purchasable { Price = 10M, StripePriceId = Prices.PremiumAnnually },
|
||||
Storage = new Purchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus.Supported },
|
||||
Subscriptions = new StripeList<Subscription> { Data = [subscription] }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade
|
||||
.GetCustomer(customerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));
|
||||
|
||||
_userRepository.GetByIdAsync(_userId).Returns(user);
|
||||
_pricingClient.GetAvailablePremiumPlan().Returns(plan);
|
||||
|
||||
// If milestone 2 is disabled, the default email is sent
|
||||
_featureService
|
||||
.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2)
|
||||
.Returns(false);
|
||||
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _userRepository.Received(1).GetByIdAsync(_userId);
|
||||
|
||||
await _mailService.Received(1).SendInvoiceUpcoming(
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains("user@example.com")),
|
||||
Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),
|
||||
Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),
|
||||
Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),
|
||||
Arg.Is<bool>(b => b == true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task
|
||||
HandleAsync_WhenUserValid_AndMilestone2Enabled_UpdatesPriceId_AndSendsUpdatedInvoiceUpcomingEmail()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var customerId = "cus_123";
|
||||
var priceSubscriptionId = "sub-1";
|
||||
var priceId = "price-id-2";
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = customerId,
|
||||
AmountDue = 10000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new() { Id = priceSubscriptionId, Price = new Price { Id = Prices.PremiumAnnually } }
|
||||
}
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus.Supported }
|
||||
},
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
var user = new User { Id = _userId, Email = "user@example.com", Premium = true };
|
||||
var plan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new Purchasable { Price = 10M, StripePriceId = priceId },
|
||||
Storage = new Purchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade
|
||||
.GetCustomer(customerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));
|
||||
|
||||
_userRepository.GetByIdAsync(_userId).Returns(user);
|
||||
_pricingClient.GetAvailablePremiumPlan().Returns(plan);
|
||||
_stripeFacade.UpdateSubscription(
|
||||
subscription.Id,
|
||||
Arg.Any<SubscriptionUpdateOptions>())
|
||||
.Returns(subscription);
|
||||
|
||||
// If milestone 2 is true, the updated invoice email is sent
|
||||
_featureService
|
||||
.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2)
|
||||
.Returns(true);
|
||||
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _userRepository.Received(1).GetByIdAsync(_userId);
|
||||
await _pricingClient.Received(1).GetAvailablePremiumPlan();
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
Arg.Is("sub_123"),
|
||||
Arg.Is<SubscriptionUpdateOptions>(o =>
|
||||
o.Items[0].Id == priceSubscriptionId &&
|
||||
o.Items[0].Price == priceId));
|
||||
|
||||
// Verify the updated invoice email was sent
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
email.ToEmails.Contains("user@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenOrganizationHasSponsorship_SendsEmail()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = "cus_123",
|
||||
AmountDue = 10000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>(),
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = "cus_123" },
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
LatestInvoiceId = "inv_latest"
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = _organizationId,
|
||||
BillingEmail = "org@example.com",
|
||||
PlanType = PlanType.EnterpriseAnnually
|
||||
};
|
||||
var plan = new FamiliesPlan();
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade
|
||||
.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||
|
||||
_organizationRepository
|
||||
.GetByIdAsync(_organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
_pricingClient
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(plan);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.IsSponsoredSubscription(subscription)
|
||||
.Returns(true);
|
||||
// Configure that this is a sponsored subscription
|
||||
_stripeEventUtilityService
|
||||
.IsSponsoredSubscription(subscription)
|
||||
.Returns(true);
|
||||
_validateSponsorshipCommand
|
||||
.ValidateSponsorshipAsync(_organizationId)
|
||||
.Returns(true);
|
||||
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _organizationRepository.Received(1).GetByIdAsync(_organizationId);
|
||||
await _validateSponsorshipCommand.Received(1).ValidateSponsorshipAsync(_organizationId);
|
||||
|
||||
await _mailService.Received(1).SendInvoiceUpcoming(
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains("org@example.com")),
|
||||
Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),
|
||||
Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),
|
||||
Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),
|
||||
Arg.Is<bool>(b => b == true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task
|
||||
HandleAsync_WhenOrganizationHasSponsorship_ButInvalidSponsorship_RetrievesUpdatedInvoice_SendsEmail()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = "cus_123",
|
||||
AmountDue = 10000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[new SubscriptionItem { Price = new Price { Id = "2021-family-for-enterprise-annually" } }]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = "cus_123" },
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
LatestInvoiceId = "inv_latest"
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = _organizationId,
|
||||
BillingEmail = "org@example.com",
|
||||
PlanType = PlanType.EnterpriseAnnually
|
||||
};
|
||||
var plan = new FamiliesPlan();
|
||||
|
||||
var paymentMethod = new Card { Last4 = "4242", Brand = "visa" };
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade
|
||||
.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||
|
||||
_organizationRepository
|
||||
.GetByIdAsync(_organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
_pricingClient
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(plan);
|
||||
|
||||
// Configure that this is not a sponsored subscription
|
||||
_stripeEventUtilityService
|
||||
.IsSponsoredSubscription(subscription)
|
||||
.Returns(true);
|
||||
|
||||
// Validate sponsorship should return false
|
||||
_validateSponsorshipCommand
|
||||
.ValidateSponsorshipAsync(_organizationId)
|
||||
.Returns(false);
|
||||
_stripeFacade
|
||||
.GetInvoice(subscription.LatestInvoiceId)
|
||||
.Returns(invoice);
|
||||
|
||||
_getPaymentMethodQuery.Run(organization).Returns(MaskedPaymentMethod.From(paymentMethod));
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _organizationRepository.Received(1).GetByIdAsync(_organizationId);
|
||||
_stripeEventUtilityService.Received(1).IsSponsoredSubscription(subscription);
|
||||
await _validateSponsorshipCommand.Received(1).ValidateSponsorshipAsync(_organizationId);
|
||||
await _stripeFacade.Received(1).GetInvoice(Arg.Is("inv_latest"));
|
||||
|
||||
await _mailService.Received(1).SendInvoiceUpcoming(
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains("org@example.com")),
|
||||
Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),
|
||||
Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),
|
||||
Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),
|
||||
Arg.Is<bool>(b => b == true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenValidOrganization_SendsEmail()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = "cus_123",
|
||||
AmountDue = 10000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[new SubscriptionItem { Price = new Price { Id = "enterprise-annually" } }]
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = "cus_123" },
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
LatestInvoiceId = "inv_latest"
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = _organizationId,
|
||||
BillingEmail = "org@example.com",
|
||||
PlanType = PlanType.EnterpriseAnnually
|
||||
};
|
||||
var plan = new FamiliesPlan();
|
||||
|
||||
var paymentMethod = new Card { Last4 = "4242", Brand = "visa" };
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade
|
||||
.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||
|
||||
_organizationRepository
|
||||
.GetByIdAsync(_organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
_pricingClient
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(plan);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.IsSponsoredSubscription(subscription)
|
||||
.Returns(false);
|
||||
|
||||
_stripeFacade
|
||||
.GetInvoice(subscription.LatestInvoiceId)
|
||||
.Returns(invoice);
|
||||
|
||||
_getPaymentMethodQuery.Run(organization).Returns(MaskedPaymentMethod.From(paymentMethod));
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _organizationRepository.Received(1).GetByIdAsync(_organizationId);
|
||||
_stripeEventUtilityService.Received(1).IsSponsoredSubscription(subscription);
|
||||
|
||||
// Should not validate sponsorship for non-sponsored subscription
|
||||
await _validateSponsorshipCommand.DidNotReceive().ValidateSponsorshipAsync(Arg.Any<Guid>());
|
||||
|
||||
await _mailService.Received(1).SendInvoiceUpcoming(
|
||||
Arg.Is<IEnumerable<string>>(emails => emails.Contains("org@example.com")),
|
||||
Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),
|
||||
Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),
|
||||
Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),
|
||||
Arg.Is<bool>(b => b == true));
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenValidProviderSubscription_SendsEmail()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = "cus_123",
|
||||
AmountDue = 10000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>(),
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = "cus_123" },
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
CollectionMethod = "charge_automatically"
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } },
|
||||
Address = new Address { Country = "UK" },
|
||||
TaxExempt = TaxExempt.None
|
||||
};
|
||||
var provider = new Provider { Id = _providerId, BillingEmail = "provider@example.com" };
|
||||
|
||||
var paymentMethod = new Card { Last4 = "4242", Brand = "visa" };
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(null, null, _providerId));
|
||||
|
||||
_providerRepository.GetByIdAsync(_providerId).Returns(provider);
|
||||
_getPaymentMethodQuery.Run(provider).Returns(MaskedPaymentMethod.From(paymentMethod));
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _providerRepository.Received(2).GetByIdAsync(_providerId);
|
||||
|
||||
// Verify tax exempt was set to reverse for non-US providers
|
||||
await _stripeFacade.Received(1).UpdateCustomer(
|
||||
Arg.Is("cus_123"),
|
||||
Arg.Is<CustomerUpdateOptions>(o => o.TaxExempt == TaxExempt.Reverse));
|
||||
|
||||
// Verify automatic tax was enabled
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
Arg.Is("sub_123"),
|
||||
Arg.Is<SubscriptionUpdateOptions>(o => o.AutomaticTax.Enabled == true));
|
||||
|
||||
// Verify provider invoice email was sent
|
||||
await _mailService.Received(1).SendProviderInvoiceUpcoming(
|
||||
Arg.Is<IEnumerable<string>>(e => e.Contains("provider@example.com")),
|
||||
Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),
|
||||
Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),
|
||||
Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),
|
||||
Arg.Is<string>(s => s == subscription.CollectionMethod),
|
||||
Arg.Is<bool>(b => b == true),
|
||||
Arg.Is<string>(s => s == $"{paymentMethod.Brand} ending in {paymentMethod.Last4}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenUpdateSubscriptionItemPriceIdFails_LogsErrorAndSendsEmail()
|
||||
{
|
||||
// Arrange
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var customerId = "cus_123";
|
||||
var priceSubscriptionId = "sub-1";
|
||||
var priceId = "price-id-2";
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = customerId,
|
||||
AmountDue = 10000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
new() { Id = priceSubscriptionId, Price = new Price { Id = Prices.PremiumAnnually } }
|
||||
}
|
||||
},
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
|
||||
Customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus.Supported }
|
||||
},
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
var user = new User { Id = _userId, Email = "user@example.com", Premium = true };
|
||||
var plan = new PremiumPlan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
LegacyYear = null,
|
||||
Seat = new Purchasable { Price = 10M, StripePriceId = priceId },
|
||||
Storage = new Purchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = customerId,
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));
|
||||
|
||||
_userRepository.GetByIdAsync(_userId).Returns(user);
|
||||
|
||||
_featureService
|
||||
.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2)
|
||||
.Returns(true);
|
||||
|
||||
_pricingClient.GetAvailablePremiumPlan().Returns(plan);
|
||||
|
||||
// Setup exception when updating subscription
|
||||
_stripeFacade
|
||||
.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
|
||||
.ThrowsAsync(new Exception());
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o =>
|
||||
o.ToString()
|
||||
.Contains(
|
||||
$"Failed to update user's ({user.Id}) subscription price id while processing event with ID {parsedEvent.Id}")),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception, string>>());
|
||||
|
||||
// Verify that email was still sent despite the exception
|
||||
await _mailer.Received(1).SendEmail(
|
||||
Arg.Is<UpdatedInvoiceUpcomingMail>(email =>
|
||||
email.ToEmails.Contains("user@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenOrganizationNotFound_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = "cus_123",
|
||||
AmountDue = 10000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>(),
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = "cus_123" },
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade
|
||||
.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||
|
||||
// Organization not found
|
||||
_organizationRepository.GetByIdAsync(_organizationId).Returns((Organization)null);
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _organizationRepository.Received(1).GetByIdAsync(_organizationId);
|
||||
|
||||
// Verify no emails were sent
|
||||
await _mailService.DidNotReceive().SendInvoiceUpcoming(
|
||||
Arg.Any<IEnumerable<string>>(),
|
||||
Arg.Any<decimal>(),
|
||||
Arg.Any<DateTime>(),
|
||||
Arg.Any<List<string>>(),
|
||||
Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenZeroAmountInvoice_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = "cus_123",
|
||||
AmountDue = 0, // Zero amount due
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Free Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>(),
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = "cus_123" },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
var user = new User { Id = _userId, Email = "user@example.com", Premium = true };
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade
|
||||
.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));
|
||||
|
||||
_userRepository.GetByIdAsync(_userId).Returns(user);
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _userRepository.Received(1).GetByIdAsync(_userId);
|
||||
|
||||
// Should not
|
||||
await _mailService.DidNotReceive().SendInvoiceUpcoming(
|
||||
Arg.Any<IEnumerable<string>>(),
|
||||
Arg.Any<decimal>(),
|
||||
Arg.Any<DateTime>(),
|
||||
Arg.Any<List<string>>(),
|
||||
Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenUserNotFound_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = "cus_123",
|
||||
AmountDue = 10000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>(),
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = "cus_123" },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade
|
||||
.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));
|
||||
|
||||
// User not found
|
||||
_userRepository.GetByIdAsync(_userId).Returns((User)null);
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _userRepository.Received(1).GetByIdAsync(_userId);
|
||||
|
||||
// Verify no emails were sent
|
||||
await _mailService.DidNotReceive().SendInvoiceUpcoming(
|
||||
Arg.Any<IEnumerable<string>>(),
|
||||
Arg.Any<decimal>(),
|
||||
Arg.Any<DateTime>(),
|
||||
Arg.Any<List<string>>(),
|
||||
Arg.Any<bool>());
|
||||
|
||||
await _mailer.DidNotReceive().SendEmail(Arg.Any<UpdatedInvoiceUpcomingMail>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenProviderNotFound_DoesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123" };
|
||||
var invoice = new Invoice
|
||||
{
|
||||
CustomerId = "cus_123",
|
||||
AmountDue = 10000,
|
||||
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
|
||||
Lines = new StripeList<InvoiceLineItem>
|
||||
{
|
||||
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = "sub_123",
|
||||
CustomerId = "cus_123",
|
||||
Items = new StripeList<SubscriptionItem>(),
|
||||
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },
|
||||
Customer = new Customer { Id = "cus_123" },
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cus_123",
|
||||
Subscriptions = new StripeList<Subscription> { Data = new List<Subscription> { subscription } }
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade
|
||||
.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())
|
||||
.Returns(customer);
|
||||
|
||||
_stripeEventUtilityService
|
||||
.GetIdsFromMetadata(subscription.Metadata)
|
||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(null, null, _providerId));
|
||||
|
||||
// Provider not found
|
||||
_providerRepository.GetByIdAsync(_providerId).Returns((Provider)null);
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _providerRepository.Received(1).GetByIdAsync(_providerId);
|
||||
|
||||
// Verify no provider emails were sent
|
||||
await _mailService.DidNotReceive().SendProviderInvoiceUpcoming(
|
||||
Arg.Any<IEnumerable<string>>(),
|
||||
Arg.Any<decimal>(),
|
||||
Arg.Any<DateTime>(),
|
||||
Arg.Any<List<string>>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<string>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user