@@ -3104,7 +3104,7 @@ await _stripeAdapter.DidNotReceive().UpdateSubscriptionAsync(
31043104 #region Schedule-Aware Tax Handling
31053105
31063106 [ Fact ]
3107- public async Task HandleAsync_WhenOrganizationTaxNotEnabled_FlagOn_SchedulePresent_UpdatesScheduleDefaultSettings ( )
3107+ public async Task HandleAsync_WhenOrganizationTaxNotEnabled_FlagOn_SchedulePresent_UpdatesSchedulePhasesAndDefaultSettings ( )
31083108 {
31093109 // Arrange
31103110 var parsedEvent = new Event { Id = "evt_123" , Type = "invoice.upcoming" } ;
@@ -3125,6 +3125,10 @@ public async Task HandleAsync_WhenOrganizationTaxNotEnabled_FlagOn_SchedulePrese
31253125 } ;
31263126 var organization = new Organization { Id = _organizationId , PlanType = PlanType . TeamsAnnually , BillingEmail = "test@test.com" } ;
31273127
3128+ var phase1Start = DateTime . UtcNow . AddDays ( - 10 ) ;
3129+ var phase1End = DateTime . UtcNow . AddDays ( 5 ) ;
3130+ var phase2End = DateTime . UtcNow . AddDays ( 370 ) ;
3131+
31283132 _stripeEventService . GetInvoice ( parsedEvent ) . Returns ( invoice ) ;
31293133 _stripeAdapter . GetCustomerAsync ( invoice . CustomerId , Arg . Any < CustomerGetOptions > ( ) ) . Returns ( customer ) ;
31303134 _stripeEventUtilityService . GetIdsFromMetadata ( subscription . Metadata )
@@ -3142,19 +3146,44 @@ public async Task HandleAsync_WhenOrganizationTaxNotEnabled_FlagOn_SchedulePrese
31423146 {
31433147 Id = "sub_sched_123" ,
31443148 SubscriptionId = "sub_123" ,
3145- Status = SubscriptionScheduleStatus . Active
3149+ Status = SubscriptionScheduleStatus . Active ,
3150+ Phases = new List < SubscriptionSchedulePhase >
3151+ {
3152+ new ( )
3153+ {
3154+ StartDate = phase1Start ,
3155+ EndDate = phase1End ,
3156+ Items = [ new SubscriptionSchedulePhaseItem { PriceId = "price_old" , Quantity = 1 } ] ,
3157+ Discounts = [ ] ,
3158+ ProrationBehavior = "none"
3159+ } ,
3160+ new ( )
3161+ {
3162+ StartDate = phase1End ,
3163+ EndDate = phase2End ,
3164+ Items = [ new SubscriptionSchedulePhaseItem { PriceId = "price_new" , Quantity = 1 } ] ,
3165+ Discounts = [ new SubscriptionSchedulePhaseDiscount { CouponId = "milestone-coupon" } ] ,
3166+ ProrationBehavior = "none"
3167+ }
3168+ }
31463169 }
31473170 ]
31483171 } ) ;
31493172
31503173 // Act
31513174 await _sut . HandleAsync ( parsedEvent ) ;
31523175
3153- // Assert — schedule's default_settings updated
3176+ // Assert — schedule updated with phases and default_settings
31543177 await _stripeAdapter . Received ( 1 ) . UpdateSubscriptionScheduleAsync (
31553178 Arg . Is ( "sub_sched_123" ) ,
31563179 Arg . Is < SubscriptionScheduleUpdateOptions > ( o =>
3157- o . DefaultSettings . AutomaticTax . Enabled == true ) ) ;
3180+ o . DefaultSettings . AutomaticTax . Enabled == true &&
3181+ o . Phases . Count == 2 &&
3182+ o . Phases [ 0 ] . AutomaticTax . Enabled == true &&
3183+ o . Phases [ 0 ] . Items [ 0 ] . Price == "price_old" &&
3184+ o . Phases [ 1 ] . AutomaticTax . Enabled == true &&
3185+ o . Phases [ 1 ] . Items [ 0 ] . Price == "price_new" &&
3186+ o . Phases [ 1 ] . Discounts [ 0 ] . Coupon == "milestone-coupon" ) ) ;
31583187
31593188 // Assert — subscription NOT updated directly for tax
31603189 await _stripeAdapter . DidNotReceive ( ) . UpdateSubscriptionAsync (
@@ -3208,7 +3237,7 @@ await _stripeAdapter.DidNotReceive().UpdateSubscriptionScheduleAsync(
32083237 }
32093238
32103239 [ Fact ]
3211- public async Task HandleAsync_WhenPremiumUserTaxNotEnabled_FlagOn_SchedulePresent_UpdatesScheduleDefaultSettings ( )
3240+ public async Task HandleAsync_WhenPremiumUserTaxNotEnabled_FlagOn_SchedulePresent_UpdatesSchedulePhasesAndDefaultSettings ( )
32123241 {
32133242 // Arrange
32143243 var parsedEvent = new Event { Id = "evt_123" , Type = "invoice.upcoming" } ;
@@ -3229,6 +3258,10 @@ public async Task HandleAsync_WhenPremiumUserTaxNotEnabled_FlagOn_SchedulePresen
32293258 } ;
32303259 var user = new User { Id = _userId , Email = "test@test.com" , Premium = true } ;
32313260
3261+ var phase1Start = DateTime . UtcNow . AddDays ( - 10 ) ;
3262+ var phase1End = DateTime . UtcNow . AddDays ( 5 ) ;
3263+ var phase2End = DateTime . UtcNow . AddDays ( 370 ) ;
3264+
32323265 _stripeEventService . GetInvoice ( parsedEvent ) . Returns ( invoice ) ;
32333266 _stripeAdapter . GetCustomerAsync ( invoice . CustomerId , Arg . Any < CustomerGetOptions > ( ) ) . Returns ( customer ) ;
32343267 _stripeEventUtilityService . GetIdsFromMetadata ( subscription . Metadata )
@@ -3245,19 +3278,44 @@ public async Task HandleAsync_WhenPremiumUserTaxNotEnabled_FlagOn_SchedulePresen
32453278 {
32463279 Id = "sub_sched_456" ,
32473280 SubscriptionId = "sub_123" ,
3248- Status = SubscriptionScheduleStatus . Active
3281+ Status = SubscriptionScheduleStatus . Active ,
3282+ Phases = new List < SubscriptionSchedulePhase >
3283+ {
3284+ new ( )
3285+ {
3286+ StartDate = phase1Start ,
3287+ EndDate = phase1End ,
3288+ Items = [ new SubscriptionSchedulePhaseItem { PriceId = "premium-annually" , Quantity = 1 } ] ,
3289+ Discounts = [ ] ,
3290+ ProrationBehavior = "none"
3291+ } ,
3292+ new ( )
3293+ {
3294+ StartDate = phase1End ,
3295+ EndDate = phase2End ,
3296+ Items = [ new SubscriptionSchedulePhaseItem { PriceId = "premium-annually-new" , Quantity = 1 } ] ,
3297+ Discounts = [ new SubscriptionSchedulePhaseDiscount { CouponId = "milestone-2c" } ] ,
3298+ ProrationBehavior = "none"
3299+ }
3300+ }
32493301 }
32503302 ]
32513303 } ) ;
32523304
32533305 // Act
32543306 await _sut . HandleAsync ( parsedEvent ) ;
32553307
3256- // Assert — schedule's default_settings updated
3308+ // Assert — schedule updated with phases and default_settings
32573309 await _stripeAdapter . Received ( 1 ) . UpdateSubscriptionScheduleAsync (
32583310 Arg . Is ( "sub_sched_456" ) ,
32593311 Arg . Is < SubscriptionScheduleUpdateOptions > ( o =>
3260- o . DefaultSettings . AutomaticTax . Enabled == true ) ) ;
3312+ o . DefaultSettings . AutomaticTax . Enabled == true &&
3313+ o . Phases . Count == 2 &&
3314+ o . Phases [ 0 ] . AutomaticTax . Enabled == true &&
3315+ o . Phases [ 0 ] . Items [ 0 ] . Price == "premium-annually" &&
3316+ o . Phases [ 1 ] . AutomaticTax . Enabled == true &&
3317+ o . Phases [ 1 ] . Items [ 0 ] . Price == "premium-annually-new" &&
3318+ o . Phases [ 1 ] . Discounts [ 0 ] . Coupon == "milestone-2c" ) ) ;
32613319
32623320 // Assert — subscription NOT updated directly for tax
32633321 await _stripeAdapter . DidNotReceive ( ) . UpdateSubscriptionAsync (
@@ -3309,6 +3367,88 @@ await _stripeAdapter.DidNotReceive().UpdateSubscriptionScheduleAsync(
33093367 Arg . Any < string > ( ) , Arg . Any < SubscriptionScheduleUpdateOptions > ( ) ) ;
33103368 }
33113369
3370+ [ Fact ]
3371+ public async Task HandleAsync_WhenTaxNotEnabled_FlagOn_Phase2Active_SkipsCompletedPhaseAndClearsConsumedDiscounts ( )
3372+ {
3373+ // Arrange — Phase 1 has ended, Phase 2 is now the active phase.
3374+ // Phase 2's one-time migration discount was consumed at transition and must not be re-included.
3375+ var parsedEvent = new Event { Id = "evt_123" , Type = "invoice.upcoming" } ;
3376+ var invoice = new Invoice { CustomerId = "cus_123" , Lines = new StripeList < InvoiceLineItem > { Data = [ ] } } ;
3377+ var subscription = new Subscription
3378+ {
3379+ Id = "sub_123" ,
3380+ CustomerId = "cus_123" ,
3381+ AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } ,
3382+ Items = new StripeList < SubscriptionItem > { Data = [ ] } ,
3383+ Metadata = new Dictionary < string , string > { { "userId" , _userId . ToString ( ) } }
3384+ } ;
3385+ var customer = new Customer
3386+ {
3387+ Id = "cus_123" ,
3388+ Subscriptions = new StripeList < Subscription > { Data = [ subscription ] } ,
3389+ Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus . Supported }
3390+ } ;
3391+ var user = new User { Id = _userId , Email = "test@test.com" , Premium = true } ;
3392+
3393+ // Phase 1 ended yesterday, Phase 2 active now
3394+ var phase1Start = DateTime . UtcNow . AddDays ( - 375 ) ;
3395+ var phase1End = DateTime . UtcNow . AddDays ( - 1 ) ;
3396+ var phase2End = DateTime . UtcNow . AddDays ( 364 ) ;
3397+
3398+ _stripeEventService . GetInvoice ( parsedEvent ) . Returns ( invoice ) ;
3399+ _stripeAdapter . GetCustomerAsync ( invoice . CustomerId , Arg . Any < CustomerGetOptions > ( ) ) . Returns ( customer ) ;
3400+ _stripeEventUtilityService . GetIdsFromMetadata ( subscription . Metadata )
3401+ . Returns ( new Tuple < Guid ? , Guid ? , Guid ? > ( null , _userId , null ) ) ;
3402+ _userRepository . GetByIdAsync ( _userId ) . Returns ( user ) ;
3403+ _featureService . IsEnabled ( FeatureFlagKeys . PM32645_DeferPriceMigrationToRenewal ) . Returns ( true ) ;
3404+
3405+ _stripeAdapter . ListSubscriptionSchedulesAsync ( Arg . Any < SubscriptionScheduleListOptions > ( ) )
3406+ . Returns ( new StripeList < SubscriptionSchedule >
3407+ {
3408+ Data =
3409+ [
3410+ new SubscriptionSchedule
3411+ {
3412+ Id = "sub_sched_789" ,
3413+ SubscriptionId = "sub_123" ,
3414+ Status = SubscriptionScheduleStatus . Active ,
3415+ Phases = new List < SubscriptionSchedulePhase >
3416+ {
3417+ new ( )
3418+ {
3419+ StartDate = phase1Start ,
3420+ EndDate = phase1End ,
3421+ Items = [ new SubscriptionSchedulePhaseItem { PriceId = "price_old" , Quantity = 1 } ] ,
3422+ Discounts = [ ] ,
3423+ ProrationBehavior = "none"
3424+ } ,
3425+ new ( )
3426+ {
3427+ StartDate = phase1End ,
3428+ EndDate = phase2End ,
3429+ Items = [ new SubscriptionSchedulePhaseItem { PriceId = "price_new" , Quantity = 1 } ] ,
3430+ Discounts = [ new SubscriptionSchedulePhaseDiscount { CouponId = "milestone-2c" } ] ,
3431+ ProrationBehavior = "none"
3432+ }
3433+ }
3434+ }
3435+ ]
3436+ } ) ;
3437+
3438+ // Act
3439+ await _sut . HandleAsync ( parsedEvent ) ;
3440+
3441+ // Assert — schedule updated: Phase 1 skipped, Phase 2 included with cleared discounts
3442+ await _stripeAdapter . Received ( 1 ) . UpdateSubscriptionScheduleAsync (
3443+ Arg . Is ( "sub_sched_789" ) ,
3444+ Arg . Is < SubscriptionScheduleUpdateOptions > ( o =>
3445+ o . DefaultSettings . AutomaticTax . Enabled == true &&
3446+ o . Phases . Count == 1 &&
3447+ o . Phases [ 0 ] . AutomaticTax . Enabled == true &&
3448+ o . Phases [ 0 ] . Items [ 0 ] . Price == "price_new" &&
3449+ o . Phases [ 0 ] . Discounts . Count == 0 ) ) ;
3450+ }
3451+
33123452 #endregion
33133453}
33143454
0 commit comments