@ -20,7 +20,6 @@ using NSubstitute;
@@ -20,7 +20,6 @@ using NSubstitute;
using NSubstitute.ReturnsExtensions ;
using Quartz ;
using Stripe ;
using Stripe.TestHelpers ;
using Xunit ;
using Event = Stripe . Event ;
@ -36,14 +35,12 @@ public class SubscriptionUpdatedHandlerTests
@@ -36,14 +35,12 @@ public class SubscriptionUpdatedHandlerTests
private readonly IUserService _ userService ;
private readonly IPushNotificationService _ pushNotificationService ;
private readonly IOrganizationRepository _ organizationRepository ;
private readonly ISchedulerFactory _ schedulerFactory ;
private readonly IOrganizationEnableCommand _ organizationEnableCommand ;
private readonly IOrganizationDisableCommand _ organizationDisableCommand ;
private readonly IPricingClient _ pricingClient ;
private readonly IFeatureService _f eatureService ;
private readonly IProviderRepository _ providerRepository ;
private readonly IProviderService _ providerService ;
private readonly ILogger < SubscriptionUpdatedHandler > _l ogger ;
private readonly IScheduler _ scheduler ;
private readonly SubscriptionUpdatedHandler _ sut ;
@ -58,18 +55,17 @@ public class SubscriptionUpdatedHandlerTests
@@ -58,18 +55,17 @@ public class SubscriptionUpdatedHandlerTests
_ providerService = Substitute . For < IProviderService > ( ) ;
_ pushNotificationService = Substitute . For < IPushNotificationService > ( ) ;
_ organizationRepository = Substitute . For < IOrganizationRepository > ( ) ;
_ providerRepository = Substitute . For < IProviderRepository > ( ) ;
_ schedulerFactory = Substitute . For < ISchedulerFactory > ( ) ;
var schedulerFactory = Substitute . For < ISchedulerFactory > ( ) ;
_ organizationEnableCommand = Substitute . For < IOrganizationEnableCommand > ( ) ;
_ organizationDisableCommand = Substitute . For < IOrganizationDisableCommand > ( ) ;
_ pricingClient = Substitute . For < IPricingClient > ( ) ;
_f eatureService = Substitute . For < IFeatureService > ( ) ;
_ providerRepository = Substitute . For < IProviderRepository > ( ) ;
_ providerService = Substitute . For < IProviderService > ( ) ;
_l ogger = Substitute . For < ILogger < SubscriptionUpdatedHandler > > ( ) ;
var l ogger = Substitute . For < ILogger < SubscriptionUpdatedHandler > > ( ) ;
_ scheduler = Substitute . For < IScheduler > ( ) ;
_ schedulerFactory . GetScheduler ( ) . Returns ( _ scheduler ) ;
schedulerFactory . GetScheduler ( ) . Returns ( _ scheduler ) ;
_ sut = new SubscriptionUpdatedHandler (
_ stripeEventService ,
@ -80,14 +76,14 @@ public class SubscriptionUpdatedHandlerTests
@@ -80,14 +76,14 @@ public class SubscriptionUpdatedHandlerTests
_ userService ,
_ pushNotificationService ,
_ organizationRepository ,
_ schedulerFactory ,
schedulerFactory ,
_ organizationEnableCommand ,
_ organizationDisableCommand ,
_ pricingClient ,
_f eatureService ,
_ providerRepository ,
_ providerService ,
_l ogger ) ;
l ogger) ;
}
[Fact]
@ -126,61 +122,115 @@ public class SubscriptionUpdatedHandlerTests
@@ -126,61 +122,115 @@ public class SubscriptionUpdatedHandlerTests
}
[Fact]
public async Task HandleAsync_UnpaidProviderSubscription_WithValidTransition_DisablesProviderAndSchedulesCancellation ( )
public async Task
HandleAsync_UnpaidProviderSubscription_WithManualSuspensionViaMetadata_DisablesProviderAndSchedulesCancellation ( )
{
// Arrange
var providerId = Guid . NewGuid ( ) ;
const string subscriptionId = "sub_123" ;
var frozenTime = DateTime . UtcNow ;
var subscriptionId = "sub_test123" ;
var testClock = new TestClock
var previousSubscription = new Subscription
{
Id = "clock_123" ,
Status = "ready" ,
FrozenTime = frozenTime
Id = subscriptionId ,
Status = StripeSubscriptionStatus . Active ,
Metadata = new Dictionary < string , string >
{
["suspend_provider"] = null // This is the key part - metadata exists, but value is null
}
} ;
var subscription = new Subscription
var currentS ubscription = new Subscription
{
Id = subscriptionId ,
Status = StripeSubscriptionStatus . Unpaid ,
Metadata = new Dictionary < string , string > { { "providerId" , providerId . ToString ( ) } } ,
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } ,
TestClock = testClock
} ;
var provider = new Provider
{
Id = providerId ,
Name = "Test Provider" ,
Enabled = true
CurrentPeriodEnd = DateTime . UtcNow . AddDays ( 3 0 ) ,
Metadata = new Dictionary < string , string >
{
["providerId"] = providerId . ToString ( ) ,
["suspend_provider"] = "true" // Now has a value, indicating manual suspension
} ,
TestClock = null
} ;
var parsedEvent = new Event
{
Id = "evt_test123" ,
Type = HandledStripeWebhook . SubscriptionUpdated ,
Data = new EventData
{
PreviousAttributes = JObject . FromObject ( new
{
status = "active"
} )
Object = currentSubscription ,
PreviousAttributes = JObject . FromObject ( previousSubscription )
}
} ;
_ stripeEventService . GetSubscription ( Arg . Any < Event > ( ) , Arg . Any < bool > ( ) , Arg . Any < List < string > > ( ) )
. Returns ( subscription ) ;
var provider = new Provider { Id = providerId , Enabled = true } ;
_ stripeEventUtilityService . GetIdsFromMetadata ( Arg . Any < Dictionary < string , string > > ( ) )
_f eatureService . IsEnabled ( FeatureFlagKeys . PM21821_ProviderPortalTakeover ) . Returns ( true ) ;
_ stripeEventService . GetSubscription ( parsedEvent , true , Arg . Any < List < string > > ( ) ) . Returns ( currentSubscription ) ;
_ stripeEventUtilityService . GetIdsFromMetadata ( currentSubscription . Metadata )
. Returns ( Tuple . Create < Guid ? , Guid ? , Guid ? > ( null , null , providerId ) ) ;
_ providerRepository . GetByIdAsync ( providerId ) . Returns ( provider ) ;
_f eatureService . IsEnabled ( FeatureFlagKeys . PM21821_ProviderPortalTakeover )
. Returns ( true ) ;
// Act
await _ sut . HandleAsync ( parsedEvent ) ;
_ providerRepository . GetByIdAsync ( providerId )
. Returns ( provider ) ;
// Assert
Assert . False ( provider . Enabled ) ;
await _ providerService . Received ( 1 ) . UpdateAsync ( provider ) ;
// Verify that UpdateSubscription was called with both CancelAt and the new metadata
await _ stripeFacade . Received ( 1 ) . UpdateSubscription (
subscriptionId ,
Arg . Is < SubscriptionUpdateOptions > ( options = >
options . CancelAt . HasValue & &
options . CancelAt . Value < = DateTime . UtcNow . AddDays ( 7 ) . AddMinutes ( 1 ) & &
options . Metadata ! = null & &
options . Metadata . ContainsKey ( "suspended_provider_via_webhook_at" ) ) ) ;
}
[Fact]
public async Task
HandleAsync_UnpaidProviderSubscription_WithValidTransition_DisablesProviderAndSchedulesCancellation ( )
{
// Arrange
var providerId = Guid . NewGuid ( ) ;
var subscriptionId = "sub_test123" ;
var previousSubscription = new Subscription
{
Id = subscriptionId ,
Status = StripeSubscriptionStatus . Active ,
Metadata = new Dictionary < string , string > { [ "providerId" ] = providerId . ToString ( ) }
} ;
var currentSubscription = new Subscription
{
Id = subscriptionId ,
Status = StripeSubscriptionStatus . Unpaid ,
CurrentPeriodEnd = DateTime . UtcNow . AddDays ( 3 0 ) ,
Metadata = new Dictionary < string , string > { [ "providerId" ] = providerId . ToString ( ) } ,
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } ,
TestClock = null
} ;
var parsedEvent = new Event
{
Id = "evt_test123" ,
Type = HandledStripeWebhook . SubscriptionUpdated ,
Data = new EventData
{
Object = currentSubscription ,
PreviousAttributes = JObject . FromObject ( previousSubscription )
}
} ;
_ stripeFacade . GetTestClock ( testClock . Id )
. Returns ( testClock ) ;
var provider = new Provider { Id = providerId , Enabled = true } ;
_f eatureService . IsEnabled ( FeatureFlagKeys . PM21821_ProviderPortalTakeover ) . Returns ( true ) ;
_ stripeEventService . GetSubscription ( parsedEvent , true , Arg . Any < List < string > > ( ) ) . Returns ( currentSubscription ) ;
_ stripeEventUtilityService . GetIdsFromMetadata ( currentSubscription . Metadata )
. Returns ( Tuple . Create < Guid ? , Guid ? , Guid ? > ( null , null , providerId ) ) ;
_ providerRepository . GetByIdAsync ( providerId ) . Returns ( provider ) ;
// Act
await _ sut . HandleAsync ( parsedEvent ) ;
@ -188,8 +238,14 @@ public class SubscriptionUpdatedHandlerTests
@@ -188,8 +238,14 @@ public class SubscriptionUpdatedHandlerTests
// Assert
Assert . False ( provider . Enabled ) ;
await _ providerService . Received ( 1 ) . UpdateAsync ( provider ) ;
await _ stripeFacade . Received ( 1 ) . UpdateSubscription ( subscriptionId ,
Arg . Is < SubscriptionUpdateOptions > ( o = > o . CancelAt = = frozenTime . AddDays ( 7 ) ) ) ;
// Verify that UpdateSubscription was called with CancelAt but WITHOUT suspension metadata
await _ stripeFacade . Received ( 1 ) . UpdateSubscription (
subscriptionId ,
Arg . Is < SubscriptionUpdateOptions > ( options = >
options . CancelAt . HasValue & &
options . CancelAt . Value < = DateTime . UtcNow . AddDays ( 7 ) . AddMinutes ( 1 ) & &
( options . Metadata = = null | | ! options . Metadata . ContainsKey ( "suspended_provider_via_webhook_at" ) ) ) ) ;
}
[Fact]
@ -207,12 +263,7 @@ public class SubscriptionUpdatedHandlerTests
@@ -207,12 +263,7 @@ public class SubscriptionUpdatedHandlerTests
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
} ;
var provider = new Provider
{
Id = providerId ,
Name = "Test Provider" ,
Enabled = true
} ;
var provider = new Provider { Id = providerId , Name = "Test Provider" , Enabled = true } ;
var parsedEvent = new Event
{
@ -220,7 +271,7 @@ public class SubscriptionUpdatedHandlerTests
@@ -220,7 +271,7 @@ public class SubscriptionUpdatedHandlerTests
{
PreviousAttributes = JObject . FromObject ( new
{
status = "unpaid" // No valid transition
status = "unpaid" // No valid transition
} )
}
} ;
@ -261,20 +312,9 @@ public class SubscriptionUpdatedHandlerTests
@@ -261,20 +312,9 @@ public class SubscriptionUpdatedHandlerTests
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
} ;
var provider = new Provider
{
Id = providerId ,
Name = "Test Provider" ,
Enabled = true
} ;
var provider = new Provider { Id = providerId , Name = "Test Provider" , Enabled = true } ;
var parsedEvent = new Event
{
Data = new EventData
{
PreviousAttributes = null
}
} ;
var parsedEvent = new Event { Data = new EventData { PreviousAttributes = null } } ;
_ stripeEventService . GetSubscription ( Arg . Any < Event > ( ) , Arg . Any < bool > ( ) , Arg . Any < List < string > > ( ) )
. Returns ( subscription ) ;
@ -314,12 +354,7 @@ public class SubscriptionUpdatedHandlerTests
@@ -314,12 +354,7 @@ public class SubscriptionUpdatedHandlerTests
LatestInvoice = new Invoice { BillingReason = "renewal" }
} ;
var provider = new Provider
{
Id = providerId ,
Name = "Test Provider" ,
Enabled = true
} ;
var provider = new Provider { Id = providerId , Name = "Test Provider" , Enabled = true } ;
var parsedEvent = new Event { Data = new EventData ( ) } ;
@ -434,10 +469,10 @@ public class SubscriptionUpdatedHandlerTests
@@ -434,10 +469,10 @@ public class SubscriptionUpdatedHandlerTests
Metadata = new Dictionary < string , string > { { "userId" , userId . ToString ( ) } } ,
Items = new StripeList < SubscriptionItem >
{
Data = new List < SubscriptionItem >
{
new ( ) { Price = new Price { Id = IStripeEventUtilityService . PremiumPlanId } }
}
Data =
[
new SubscriptionItem { Price = new Price { Id = IStripeEventUtilityService . PremiumPlanId } }
]
}
} ;
@ -478,11 +513,7 @@ public class SubscriptionUpdatedHandlerTests
@@ -478,11 +513,7 @@ public class SubscriptionUpdatedHandlerTests
Metadata = new Dictionary < string , string > { { "organizationId" , organizationId . ToString ( ) } }
} ;
var organization = new Organization
{
Id = organizationId ,
PlanType = PlanType . EnterpriseAnnually2023
} ;
var organization = new Organization { Id = organizationId , PlanType = PlanType . EnterpriseAnnually2023 } ;
var parsedEvent = new Event { Data = new EventData ( ) } ;
_ stripeEventService . GetSubscription ( Arg . Any < Event > ( ) , Arg . Any < bool > ( ) , Arg . Any < List < string > > ( ) )
@ -495,7 +526,7 @@ public class SubscriptionUpdatedHandlerTests
@@ -495,7 +526,7 @@ public class SubscriptionUpdatedHandlerTests
. Returns ( organization ) ;
_ stripeFacade . ListInvoices ( Arg . Any < InvoiceListOptions > ( ) )
. Returns ( new StripeList < Invoice > { Data = new List < Invoice > { new Invoice { Id = "inv_123" } } } ) ;
. Returns ( new StripeList < Invoice > { Data = [ new Invoice { Id = "inv_123" } ] } ) ;
var plan = new Enterprise2023Plan ( true ) ;
_ pricingClient . GetPlanOrThrow ( organization . PlanType )
@ -577,7 +608,8 @@ public class SubscriptionUpdatedHandlerTests
@@ -577,7 +608,8 @@ public class SubscriptionUpdatedHandlerTests
}
[Fact]
public async Task HandleAsync_WhenSubscriptionIsActive_AndOrganizationHasSecretsManagerTrial_AndRemovingSecretsManagerTrial_RemovesPasswordManagerCoupon ( )
public async Task
HandleAsync_WhenSubscriptionIsActive_AndOrganizationHasSecretsManagerTrial_AndRemovingSecretsManagerTrial_RemovesPasswordManagerCoupon ( )
{
// Arrange
var organizationId = Guid . NewGuid ( ) ;
@ -589,34 +621,18 @@ public class SubscriptionUpdatedHandlerTests
@@ -589,34 +621,18 @@ public class SubscriptionUpdatedHandlerTests
CustomerId = "cus_123" ,
Items = new StripeList < SubscriptionItem >
{
Data = new List < SubscriptionItem >
{
new ( ) { Plan = new Stripe . Plan { Id = "2023-enterprise-org-seat-annually" } }
}
Data = [ new SubscriptionItem { Plan = new Plan { Id = "2023-enterprise-org-seat-annually" } } ]
} ,
Customer = new Customer
{
Balance = 0 ,
Discount = new Discount
{
Coupon = new Coupon { Id = "sm-standalone" }
}
} ,
Discount = new Discount
{
Coupon = new Coupon { Id = "sm-standalone" }
Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } }
} ,
Metadata = new Dictionary < string , string >
{
{ "organizationId" , organizationId . ToString ( ) }
}
Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } } ,
Metadata = new Dictionary < string , string > { { "organizationId" , organizationId . ToString ( ) } }
} ;
var organization = new Organization
{
Id = organizationId ,
PlanType = PlanType . EnterpriseAnnually2023
} ;
var organization = new Organization { Id = organizationId , PlanType = PlanType . EnterpriseAnnually2023 } ;
var plan = new Enterprise2023Plan ( true ) ;
_ pricingClient . GetPlanOrThrow ( organization . PlanType )
@ -631,20 +647,14 @@ public class SubscriptionUpdatedHandlerTests
@@ -631,20 +647,14 @@ public class SubscriptionUpdatedHandlerTests
{
items = new
{
data = new [ ]
{
new { plan = new { id = "secrets-manager-enterprise-seat-annually" } }
}
data = new [ ] { new { plan = new { id = "secrets-manager-enterprise-seat-annually" } } }
} ,
Items = new StripeList < SubscriptionItem >
{
Data = new List < SubscriptionItem >
{
new SubscriptionItem
{
Plan = new Stripe . Plan { Id = "secrets-manager-enterprise-seat-annually" }
}
}
Data =
[
new SubscriptionItem { Plan = new Plan { Id = "secrets-manager-enterprise-seat-annually" } }
]
}
} )
}
@ -990,7 +1000,7 @@ public class SubscriptionUpdatedHandlerTests
@@ -990,7 +1000,7 @@ public class SubscriptionUpdatedHandlerTests
{
Id = previousSubscription ? . Id ? ? "sub_123" ,
Status = StripeSubscriptionStatus . Active ,
Metadata = new Dictionary < string , string > { { "providerId" , providerId . ToString ( ) } } ,
Metadata = new Dictionary < string , string > { { "providerId" , providerId . ToString ( ) } }
} ;
var provider = new Provider { Id = providerId , Enabled = false } ;
@ -1010,10 +1020,10 @@ public class SubscriptionUpdatedHandlerTests
@@ -1010,10 +1020,10 @@ public class SubscriptionUpdatedHandlerTests
{
return new List < object [ ] >
{
new object [ ] { new Subscription { Id = "sub_123" , Status = StripeSubscriptionStatus . Unpaid } , } ,
new object [ ] { new Subscription { Id = "sub_123" , Status = StripeSubscriptionStatus . Incomplete } , } ,
new object [ ] { new Subscription { Id = "sub_123" , Status = StripeSubscriptionStatus . IncompleteExpired } , } ,
new object [ ] { new Subscription { Id = "sub_123" , Status = StripeSubscriptionStatus . Paused } , } ,
new object [ ] { new Subscription { Id = "sub_123" , Status = StripeSubscriptionStatus . Unpaid } } ,
new object [ ] { new Subscription { Id = "sub_123" , Status = StripeSubscriptionStatus . Incomplete } } ,
new object [ ] { new Subscription { Id = "sub_123" , Status = StripeSubscriptionStatus . IncompleteExpired } } ,
new object [ ] { new Subscription { Id = "sub_123" , Status = StripeSubscriptionStatus . Paused } }
} ;
}
}