mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-24298] milestone3 families 2025 (#6586)
* [PM-24298] milestone3 families2025 # Conflicts: # src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs # test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs * update test name
This commit is contained in:
@@ -195,41 +195,48 @@ public class UpcomingInvoiceHandler(
|
||||
Plan plan,
|
||||
bool milestone3)
|
||||
{
|
||||
if (milestone3 && plan.Type == PlanType.FamiliesAnnually2019)
|
||||
// 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))
|
||||
{
|
||||
var passwordManagerItem =
|
||||
subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordManagerItem == null)
|
||||
{
|
||||
logger.LogWarning("Could not find Organization's ({OrganizationId}) password manager item while processing '{EventType}' event ({EventID})",
|
||||
organization.Id, @event.Type, @event.Id);
|
||||
return;
|
||||
}
|
||||
var passwordManagerItem =
|
||||
subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId);
|
||||
|
||||
var families = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually);
|
||||
if (passwordManagerItem == null)
|
||||
{
|
||||
logger.LogWarning("Could not find Organization's ({OrganizationId}) password manager item while processing '{EventType}' event ({EventID})",
|
||||
organization.Id, @event.Type, @event.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
organization.PlanType = families.Type;
|
||||
organization.Plan = families.Name;
|
||||
organization.UsersGetPremium = families.UsersGetPremium;
|
||||
organization.Seats = families.PasswordManager.BaseSeats;
|
||||
var families = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually);
|
||||
|
||||
var options = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Id = passwordManagerItem.Id,
|
||||
organization.PlanType = families.Type;
|
||||
organization.Plan = families.Name;
|
||||
organization.UsersGetPremium = families.UsersGetPremium;
|
||||
organization.Seats = families.PasswordManager.BaseSeats;
|
||||
|
||||
var options = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Id = passwordManagerItem.Id,
|
||||
Price = families.PasswordManager.StripePlanId
|
||||
}
|
||||
],
|
||||
Discounts =
|
||||
[
|
||||
new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone3SubscriptionDiscount }
|
||||
],
|
||||
ProrationBehavior = ProrationBehavior.None
|
||||
};
|
||||
}
|
||||
],
|
||||
ProrationBehavior = ProrationBehavior.None
|
||||
};
|
||||
|
||||
if (plan.Type == PlanType.FamiliesAnnually2019)
|
||||
{
|
||||
options.Discounts =
|
||||
[
|
||||
new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone3SubscriptionDiscount }
|
||||
];
|
||||
|
||||
var premiumAccessAddOnItem = subscription.Items.FirstOrDefault(item =>
|
||||
item.Price.Id == plan.PasswordManager.StripePremiumAccessPlanId);
|
||||
@@ -253,21 +260,21 @@ public class UpcomingInvoiceHandler(
|
||||
Deleted = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
await stripeFacade.UpdateSubscription(subscription.Id, options);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to align subscription concerns for Organization ({OrganizationID}) while processing '{EventType}' event ({EventID})",
|
||||
organization.Id,
|
||||
@event.Type,
|
||||
@event.Id);
|
||||
}
|
||||
try
|
||||
{
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
await stripeFacade.UpdateSubscription(subscription.Id, options);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to align subscription concerns for Organization ({OrganizationID}) while processing '{EventType}' event ({EventID})",
|
||||
organization.Id,
|
||||
@event.Type,
|
||||
@event.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1141,7 +1141,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenMilestone3Disabled_DoesNotUpdateSubscription()
|
||||
public async Task HandleAsync_WhenMilestone3Disabled_AndFamilies2019Plan_DoesNotUpdateSubscription()
|
||||
{
|
||||
// Arrange
|
||||
var parsedEvent = new Event { Id = "evt_123", Type = "invoice.upcoming" };
|
||||
@@ -1789,4 +1789,170 @@ public class UpcomingInvoiceHandlerTests
|
||||
email.ToEmails.Contains("org@example.com") &&
|
||||
email.Subject == "Your Subscription Will Renew Soon"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WhenMilestone3Enabled_AndFamilies2025Plan_UpdatesSubscriptionOnlyNoAddons()
|
||||
{
|
||||
// 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 List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
|
||||
var families2025Plan = new Families2025Plan();
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
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 = new List<Subscription> { subscription } },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = _organizationId,
|
||||
BillingEmail = "org@example.com",
|
||||
PlanType = PlanType.FamiliesAnnually2025
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade.GetCustomer(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);
|
||||
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);
|
||||
_stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _stripeFacade.Received(1).UpdateSubscription(
|
||||
Arg.Is(subscriptionId),
|
||||
Arg.Is<SubscriptionUpdateOptions>(o =>
|
||||
o.Items.Count == 1 &&
|
||||
o.Items[0].Id == passwordManagerItemId &&
|
||||
o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&
|
||||
o.Discounts == null &&
|
||||
o.ProrationBehavior == ProrationBehavior.None));
|
||||
|
||||
await _organizationRepository.Received(1).ReplaceAsync(
|
||||
Arg.Is<Organization>(org =>
|
||||
org.Id == _organizationId &&
|
||||
org.PlanType == PlanType.FamiliesAnnually &&
|
||||
org.Plan == familiesPlan.Name &&
|
||||
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
|
||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||
}
|
||||
|
||||
[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 List<InvoiceLineItem> { new() { Description = "Test Item" } }
|
||||
}
|
||||
};
|
||||
|
||||
var families2025Plan = new Families2025Plan();
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
CustomerId = customerId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
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 = new List<Subscription> { subscription } },
|
||||
Address = new Address { Country = "US" }
|
||||
};
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = _organizationId,
|
||||
BillingEmail = "org@example.com",
|
||||
PlanType = PlanType.FamiliesAnnually2025
|
||||
};
|
||||
|
||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||
_stripeFacade.GetCustomer(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 _stripeFacade.DidNotReceive().UpdateSubscription(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<SubscriptionUpdateOptions>());
|
||||
|
||||
await _organizationRepository.DidNotReceive().ReplaceAsync(
|
||||
Arg.Is<Organization>(org => org.PlanType == PlanType.FamiliesAnnually));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user