Skip to content
12 changes: 3 additions & 9 deletions src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,11 @@ private async Task HandleOrganizationUpcomingInvoiceAsync(

var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);

var milestone3 = featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3);

var subscriptionAligned = await AlignOrganizationSubscriptionConcernsAsync(
organization,
@event,
subscription,
plan,
milestone3);
plan);

/*
* Subscription alignment sends out a different version of our Upcoming Invoice email, so we don't need to continue
Expand Down Expand Up @@ -210,17 +207,14 @@ await stripeAdapter.UpdateCustomerAsync(subscription.CustomerId,
/// <param name="event">The Stripe event associated with this operation.</param>
/// <param name="subscription">The organization's subscription.</param>
/// <param name="plan">The organization's current plan.</param>
/// <param name="milestone3">A flag indicating whether the third milestone is enabled.</param>
/// <returns>Whether the operation resulted in an updated subscription.</returns>
private async Task<bool> AlignOrganizationSubscriptionConcernsAsync(
Organization organization,
Event @event,
Subscription subscription,
Plan plan,
bool milestone3)
Plan plan)
{
// currently these are the only plans that need aligned and both require the same flag and share most of the logic
if (!milestone3 || plan.Type is not (PlanType.FamiliesAnnually2019 or PlanType.FamiliesAnnually2025))
if (plan.Type is not (PlanType.FamiliesAnnually2019 or PlanType.FamiliesAnnually2025))
{
return false;
}
Expand Down
26 changes: 3 additions & 23 deletions src/Core/Billing/Pricing/PricingClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing.Organizations;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;

Expand All @@ -13,7 +12,6 @@ namespace Bit.Core.Billing.Pricing;
using PremiumPlan = Premium.Plan;

public class PricingClient(
IFeatureService featureService,
GlobalSettings globalSettings,
HttpClient httpClient,
ILogger<PricingClient> logger) : IPricingClient
Expand All @@ -40,7 +38,7 @@ public class PricingClient(
var plan = await response.Content.ReadFromJsonAsync<Plan>();
return plan == null
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
: new PlanAdapter(PreProcessFamiliesPreMigrationPlan(plan));
: new PlanAdapter(plan);
}

if (response.StatusCode == HttpStatusCode.NotFound)
Expand Down Expand Up @@ -74,7 +72,7 @@ public async Task<List<OrganizationPlan>> ListPlans()
var plans = await response.Content.ReadFromJsonAsync<List<Plan>>();
return plans == null
? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null")
: plans.Select(OrganizationPlan (plan) => new PlanAdapter(PreProcessFamiliesPreMigrationPlan(plan))).ToList();
: plans.Select(OrganizationPlan (plan) => new PlanAdapter(plan)).ToList();
}

throw new BillingException(
Expand Down Expand Up @@ -121,10 +119,7 @@ public async Task<List<PremiumPlan>> ListPremiumPlans()
PlanType.EnterpriseMonthly2020 => "enterprise-monthly-2020",
PlanType.EnterpriseMonthly2023 => "enterprise-monthly-2023",
PlanType.FamiliesAnnually => "families",
PlanType.FamiliesAnnually2025 =>
featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3)
? "families-2025"
: "families",
PlanType.FamiliesAnnually2025 => "families-2025",
PlanType.FamiliesAnnually2019 => "families-2019",
PlanType.Free => "free",
PlanType.TeamsAnnually => "teams-annually",
Expand All @@ -139,19 +134,4 @@ public async Task<List<PremiumPlan>> ListPremiumPlans()
PlanType.TeamsStarter2023 => "teams-starter-2023",
_ => null
};

/// <summary>
/// Safeguard used until the feature flag is enabled. Pricing service will return the
/// 2025PreMigration plan with "families" lookup key. When that is detected and the FF
/// is still disabled, set the lookup key to families-2025 so PlanAdapter will assign
/// the correct plan.
/// </summary>
/// <param name="plan">The plan to preprocess</param>
private Plan PreProcessFamiliesPreMigrationPlan(Plan plan)
{
if (plan.LookupKey == "families" && !featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3))
plan.LookupKey = "families-2025";
return plan;
}

}
171 changes: 0 additions & 171 deletions test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1419,7 +1419,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_AndFamilies2019Plan_UpdatesS
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Act
Expand Down Expand Up @@ -1527,7 +1526,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_AndFamilies2019Plan_WithoutP
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Act
Expand Down Expand Up @@ -1558,88 +1556,6 @@ await _stripeAdapter.DidNotReceive().UpdateCustomerAsync(
Arg.Any<CustomerUpdateOptions>());
}

[Fact]
public async Task HandleAsync_WhenMilestone3Disabled_AndFamilies2019Plan_DoesNotUpdateSubscription()
{
// Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
var customerId = "cus_123";
var subscriptionId = "sub_123";
var passwordManagerItemId = "si_pm_123";

var invoice = new Invoice
{
CustomerId = customerId,
AmountDue = 40000,
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
Lines = new StripeList<InvoiceLineItem>
{
Data = [new() { Description = "Test Item" }]
}
};

var families2019Plan = new Families2019Plan();

var subscription = new Subscription
{
Id = subscriptionId,
CustomerId = customerId,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new()
{
Id = passwordManagerItemId,
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
}
]
},
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
Metadata = new Dictionary<string, string>()
};

var customer = new Customer
{
Id = customerId,
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
Address = new Address { Country = "US" }
};

var organization = new Organization
{
Id = _organizationId,
BillingEmail = "org@example.com",
PlanType = PlanType.FamiliesAnnually2019
};

_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
_stripeAdapter.GetCustomerAsync(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(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Act
await _sut.HandleAsync(parsedEvent);

// Assert - should not update subscription or organization when feature flag is disabled
await _stripeAdapter.DidNotReceive().UpdateSubscriptionAsync(
Arg.Any<string>(),
Arg.Is<SubscriptionUpdateOptions>(o => o.Discounts != null));

await _organizationRepository.DidNotReceive().ReplaceAsync(
Arg.Is<Organization>(org => org.PlanType == PlanType.FamiliesAnnually));

// Families plan is excluded from tax exempt alignment
await _stripeAdapter.DidNotReceive().UpdateCustomerAsync(
Arg.Any<string>(),
Arg.Any<CustomerUpdateOptions>());
}

[Fact]
public async Task HandleAsync_WhenMilestone3Enabled_ButNotFamilies2019Plan_DoesNotUpdateSubscription()
{
Expand Down Expand Up @@ -1697,7 +1613,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_ButNotFamilies2019Plan_DoesN
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Act
Expand Down Expand Up @@ -1772,7 +1687,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_AndPasswordManagerItemNotFou
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Act
Expand Down Expand Up @@ -1860,7 +1774,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_AndUpdateFails_LogsErrorAndS
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Simulate update failure
Expand Down Expand Up @@ -1958,7 +1871,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_AndCouponNotFound_LogsErrorA
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
_stripeAdapter.GetCouponAsync(CouponIDs.Milestone3SubscriptionDiscount).Returns((Coupon)null);
_stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
Expand Down Expand Up @@ -2058,7 +1970,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_AndCouponPercentOffIsNull_Lo
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
_stripeAdapter.GetCouponAsync(CouponIDs.Milestone3SubscriptionDiscount).Returns(coupon);
_stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
Expand Down Expand Up @@ -2163,7 +2074,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_AndSeatAddOnExists_DeletesIt
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Act
Expand Down Expand Up @@ -2277,7 +2187,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_AndSeatAddOnWithQuantityOne_
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Act
Expand Down Expand Up @@ -2398,7 +2307,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_WithPremiumAccessAndSeatAddO
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Act
Expand Down Expand Up @@ -2503,7 +2411,6 @@ public async Task HandleAsync_WhenMilestone3Enabled_AndFamilies2025Plan_UpdatesS
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2025).Returns(families2025Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Act
Expand Down Expand Up @@ -2534,83 +2441,6 @@ await _mailer.Received(1).SendEmail(
email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))));
}

[Fact]
public async Task HandleAsync_WhenMilestone3Disabled_AndFamilies2025Plan_DoesNotUpdateSubscription()
{
// Arrange
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
var customerId = "cus_123";
var subscriptionId = "sub_123";
var passwordManagerItemId = "si_pm_123";

var invoice = new Invoice
{
CustomerId = customerId,
AmountDue = 40000,
NextPaymentAttempt = DateTime.UtcNow.AddDays(7),
Lines = new StripeList<InvoiceLineItem>
{
Data = [new() { Description = "Test Item" }]
}
};

var families2025Plan = new Families2025Plan();

var subscription = new Subscription
{
Id = subscriptionId,
CustomerId = customerId,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new()
{
Id = passwordManagerItemId,
Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId }
}
]
},
AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },
Metadata = new Dictionary<string, string>()
};

var customer = new Customer
{
Id = customerId,
Subscriptions = new StripeList<Subscription> { Data = [subscription] },
Address = new Address { Country = "US" }
};

var organization = new Organization
{
Id = _organizationId,
BillingEmail = "org@example.com",
PlanType = PlanType.FamiliesAnnually2025
};

_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
_stripeAdapter.GetCustomerAsync(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(PlanType.FamiliesAnnually2025).Returns(families2025Plan);
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);

// Act
await _sut.HandleAsync(parsedEvent);

// Assert - should not update subscription or organization when feature flag is disabled
await _stripeAdapter.DidNotReceive().UpdateSubscriptionAsync(
Arg.Any<string>(),
Arg.Any<SubscriptionUpdateOptions>());

await _organizationRepository.DidNotReceive().ReplaceAsync(
Arg.Is<Organization>(org => org.PlanType == PlanType.FamiliesAnnually));
}

#region Premium Renewal Email Tests

[Fact]
Expand Down Expand Up @@ -3084,7 +2914,6 @@ public async Task HandleAsync_Families_DeferEnabled_CallsScheduler()
_organizationRepository.GetByIdAsync(_organizationId).Returns(organization);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(new FamiliesPlan());
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
_featureService.IsEnabled(FeatureFlagKeys.PM32645_DeferPriceMigrationToRenewal).Returns(true);
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
_stripeAdapter.GetCouponAsync(CouponIDs.Milestone3SubscriptionDiscount)
Expand Down
Loading
Loading