Skip to content

Commit 8d30fbc

Browse files
Billing/pm 30882/defect pm coupon removed on upgrade (#6863)
* fix(billing): update coupon check logic * tests(billing): update tests and add plan check test
1 parent aa33a67 commit 8d30fbc

2 files changed

Lines changed: 98 additions & 2 deletions

File tree

src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,17 +275,24 @@ private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync
275275
.PreviousAttributes
276276
.ToObject<Subscription>() as Subscription;
277277

278+
// Get all plan IDs that include Secrets Manager support to check if the organization has secret manager in the
279+
// previous and/or current subscriptions.
280+
var planIdsOfPlansWithSecretManager = (await _pricingClient.ListPlans())
281+
.Where(orgPlan => orgPlan.SupportsSecretsManager && orgPlan.SecretsManager.StripeSeatPlanId != null)
282+
.Select(orgPlan => orgPlan.SecretsManager.StripeSeatPlanId)
283+
.ToHashSet();
284+
278285
// This being false doesn't necessarily mean that the organization doesn't subscribe to Secrets Manager.
279286
// If there are changes to any subscription item, Stripe sends every item in the subscription, both
280287
// changed and unchanged.
281288
var previousSubscriptionHasSecretsManager =
282289
previousSubscription?.Items is not null &&
283290
previousSubscription.Items.Any(
284-
previousSubscriptionItem => previousSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
291+
previousSubscriptionItem => planIdsOfPlansWithSecretManager.Contains(previousSubscriptionItem.Plan.Id));
285292

286293
var currentSubscriptionHasSecretsManager =
287294
subscription.Items.Any(
288-
currentSubscriptionItem => currentSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
295+
currentSubscriptionItem => planIdsOfPlansWithSecretManager.Contains(currentSubscriptionItem.Plan.Id));
289296

290297
if (!previousSubscriptionHasSecretsManager || currentSubscriptionHasSecretsManager)
291298
{

test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
1212
using Bit.Core.Repositories;
1313
using Bit.Core.Services;
14+
using Bit.Core.Test.Billing.Mocks;
1415
using Bit.Core.Test.Billing.Mocks.Plans;
1516
using Microsoft.Extensions.Logging;
1617
using Newtonsoft.Json.Linq;
@@ -654,6 +655,8 @@ public async Task
654655
var plan = new Enterprise2023Plan(true);
655656
_pricingClient.GetPlanOrThrow(organization.PlanType)
656657
.Returns(plan);
658+
_pricingClient.ListPlans()
659+
.Returns(MockPlans.Plans);
657660

658661
var parsedEvent = new Event
659662
{
@@ -693,6 +696,92 @@ public async Task
693696
await _stripeFacade.Received(1).DeleteCustomerDiscount(subscription.CustomerId);
694697
await _stripeFacade.Received(1).DeleteSubscriptionDiscount(subscription.Id);
695698
}
699+
[Fact]
700+
public async Task
701+
HandleAsync_WhenUpgradingPlan_AndPreviousPlanHasSecretsManagerTrial_AndCurrentPlanHasSecretsManagerTrial_DoesNotRemovePasswordManagerCoupon()
702+
{
703+
// Arrange
704+
var organizationId = Guid.NewGuid();
705+
var subscription = new Subscription
706+
{
707+
Id = "sub_123",
708+
Status = StripeSubscriptionStatus.Active,
709+
CustomerId = "cus_123",
710+
Items = new StripeList<SubscriptionItem>
711+
{
712+
Data =
713+
[
714+
new SubscriptionItem
715+
{
716+
CurrentPeriodEnd = DateTime.UtcNow.AddDays(10),
717+
Plan = new Plan { Id = "2023-enterprise-org-seat-annually" }
718+
},
719+
new SubscriptionItem
720+
{
721+
CurrentPeriodEnd = DateTime.UtcNow.AddDays(10),
722+
Plan = new Plan { Id = "secrets-manager-enterprise-seat-annually" }
723+
}
724+
]
725+
},
726+
Customer = new Customer
727+
{
728+
Balance = 0,
729+
Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } }
730+
},
731+
Discounts = [new Discount { Coupon = new Coupon { Id = "sm-standalone" } }],
732+
Metadata = new Dictionary<string, string> { { "organizationId", organizationId.ToString() } }
733+
};
734+
735+
// Note: The organization plan is still the previous plan because the subscription is updated before the organization is updated
736+
var organization = new Organization { Id = organizationId, PlanType = PlanType.TeamsAnnually2023 };
737+
738+
var plan = new Teams2023Plan(true);
739+
_pricingClient.GetPlanOrThrow(organization.PlanType)
740+
.Returns(plan);
741+
_pricingClient.ListPlans()
742+
.Returns(MockPlans.Plans);
743+
744+
var parsedEvent = new Event
745+
{
746+
Data = new EventData
747+
{
748+
Object = subscription,
749+
PreviousAttributes = JObject.FromObject(new
750+
{
751+
items = new
752+
{
753+
data = new[]
754+
{
755+
new { plan = new { id = "secrets-manager-teams-seat-annually" } },
756+
}
757+
},
758+
Items = new StripeList<SubscriptionItem>
759+
{
760+
Data =
761+
[
762+
new SubscriptionItem { Plan = new Stripe.Plan { Id = "secrets-manager-teams-seat-annually" } },
763+
]
764+
}
765+
})
766+
}
767+
};
768+
769+
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
770+
.Returns(subscription);
771+
772+
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
773+
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(organizationId, null, null));
774+
775+
_organizationRepository.GetByIdAsync(organizationId)
776+
.Returns(organization);
777+
778+
// Act
779+
await _sut.HandleAsync(parsedEvent);
780+
781+
// Assert
782+
await _stripeFacade.DidNotReceive().DeleteCustomerDiscount(subscription.CustomerId);
783+
await _stripeFacade.DidNotReceive().DeleteSubscriptionDiscount(subscription.Id);
784+
}
696785

697786
[Theory]
698787
[MemberData(nameof(GetNonActiveSubscriptions))]

0 commit comments

Comments
 (0)