@@ -391,6 +391,101 @@ describe('Billing service unit tests:', () => {
391391
392392 expect ( url ) . toBe ( 'https://checkout.stripe.com/session_123' ) ;
393393 } ) ;
394+
395+ // ── Stripe-side live guard (trialing race window) ──────────────────────────────────────────
396+
397+ test ( 'should throw 409 subscription_already_active when Stripe lists a trialing sub but DB shows none' , async ( ) => {
398+ jest . unstable_mockModule ( '../../../config/index.js' , ( ) => ( {
399+ default : { stripe : { secretKey : 'sk_test_live_trialing' } } ,
400+ } ) ) ;
401+
402+ // DB shows no active sub locally — only the Stripe live check should catch this
403+ mockSubscriptionRepository . findByOrganization . mockResolvedValue ( {
404+ organization : orgId ,
405+ stripeCustomerId : 'cus_x' ,
406+ stripeSubscriptionId : null ,
407+ plan : 'free' ,
408+ status : 'canceled' ,
409+ } ) ;
410+
411+ // Stripe live check returns a trialing sub
412+ mockStripeInstance . subscriptions . list . mockResolvedValue ( {
413+ data : [ { id : 'sub_trial_x' , status : 'trialing' } ] ,
414+ } ) ;
415+
416+ const mod = await import ( '../services/billing.service.js' ) ;
417+ BillingService = mod . default ;
418+
419+ await expect (
420+ BillingService . createCheckout ( mockOrganization , 'price_starter_m' , 'http://ok' , 'http://cancel' ) ,
421+ ) . rejects . toMatchObject ( { code : 'subscription_already_active' , statusCode : 409 } ) ;
422+
423+ // Verify the live Stripe check was called with the correct params
424+ expect ( mockStripeInstance . subscriptions . list ) . toHaveBeenCalledWith (
425+ expect . objectContaining ( { customer : 'cus_x' , status : 'all' , limit : 10 } ) ,
426+ ) ;
427+ expect ( mockStripeInstance . checkout . sessions . create ) . not . toHaveBeenCalled ( ) ;
428+ } ) ;
429+
430+ test ( 'should throw 409 subscription_already_active when Stripe lists an active sub but DB shows none' , async ( ) => {
431+ jest . unstable_mockModule ( '../../../config/index.js' , ( ) => ( {
432+ default : { stripe : { secretKey : 'sk_test_live_active' } } ,
433+ } ) ) ;
434+
435+ // DB shows no active sub locally — only the Stripe live check should catch this
436+ mockSubscriptionRepository . findByOrganization . mockResolvedValue ( {
437+ organization : orgId ,
438+ stripeCustomerId : 'cus_x' ,
439+ stripeSubscriptionId : null ,
440+ plan : 'free' ,
441+ status : 'canceled' ,
442+ } ) ;
443+
444+ // Stripe live check returns an active sub
445+ mockStripeInstance . subscriptions . list . mockResolvedValue ( {
446+ data : [ { id : 'sub_active_x' , status : 'active' } ] ,
447+ } ) ;
448+
449+ const mod = await import ( '../services/billing.service.js' ) ;
450+ BillingService = mod . default ;
451+
452+ await expect (
453+ BillingService . createCheckout ( mockOrganization , 'price_starter_m' , 'http://ok' , 'http://cancel' ) ,
454+ ) . rejects . toMatchObject ( { code : 'subscription_already_active' , statusCode : 409 } ) ;
455+
456+ expect ( mockStripeInstance . subscriptions . list ) . toHaveBeenCalledWith (
457+ expect . objectContaining ( { customer : 'cus_x' , status : 'all' , limit : 10 } ) ,
458+ ) ;
459+ expect ( mockStripeInstance . checkout . sessions . create ) . not . toHaveBeenCalled ( ) ;
460+ } ) ;
461+
462+ test ( 'should NOT block checkout when Stripe lists an incomplete sub (incomplete is not in block list)' , async ( ) => {
463+ jest . unstable_mockModule ( '../../../config/index.js' , ( ) => ( {
464+ default : { stripe : { secretKey : 'sk_test_live_incomplete' } } ,
465+ } ) ) ;
466+
467+ // DB shows no active sub locally
468+ mockSubscriptionRepository . findByOrganization . mockResolvedValue ( {
469+ organization : orgId ,
470+ stripeCustomerId : 'cus_x' ,
471+ stripeSubscriptionId : null ,
472+ plan : 'free' ,
473+ status : 'canceled' ,
474+ } ) ;
475+
476+ // Stripe returns only an incomplete sub — should NOT block (deliberate asymmetry vs DB guard)
477+ mockStripeInstance . subscriptions . list . mockResolvedValue ( {
478+ data : [ { id : 'sub_incomplete_x' , status : 'incomplete' } ] ,
479+ } ) ;
480+
481+ const mod = await import ( '../services/billing.service.js' ) ;
482+ BillingService = mod . default ;
483+
484+ const url = await BillingService . createCheckout ( mockOrganization , 'price_starter_m' , 'http://ok' , 'http://cancel' ) ;
485+
486+ expect ( url ) . toBe ( 'https://checkout.stripe.com/session_123' ) ;
487+ expect ( mockStripeInstance . checkout . sessions . create ) . toHaveBeenCalled ( ) ;
488+ } ) ;
394489 } ) ;
395490
396491 describe ( 'createPortalSession' , ( ) => {
0 commit comments