@@ -47,6 +47,7 @@ const makeLicense = (overrides: Partial<License> = {}): License => ({
4747 trialEnd : null ,
4848 hasPaymentMethod : null ,
4949 lastSyncAt : NOW ,
50+ lastSyncErrorCode : null ,
5051 createdAt : NOW ,
5152 updatedAt : NOW ,
5253 ...overrides ,
@@ -447,6 +448,81 @@ describe('resolveActiveBanner', () => {
447448 } ) ;
448449 } ) ;
449450
451+ describe ( 'license rebound elsewhere' , ( ) => {
452+ test ( 'lastSyncErrorCode is rebound code → banner (non-dismissible, everyone)' , ( ) => {
453+ const result = resolveActiveBanner ( makeContext ( {
454+ license : makeLicense ( {
455+ status : 'active' ,
456+ lastSyncErrorCode : 'ACTIVATION_CODE_BOUND_TO_DIFFERENT_INSTANCE' ,
457+ } ) ,
458+ } ) ) ;
459+ expect ( result ?. id ) . toBe ( 'licenseReboundElsewhere' ) ;
460+ expect ( result ?. dismissible ) . toBe ( false ) ;
461+ expect ( result ?. audience ) . toBe ( 'everyone' ) ;
462+ } ) ;
463+
464+ test ( 'null lastSyncErrorCode → no rebound banner' , ( ) => {
465+ const result = resolveActiveBanner ( makeContext ( {
466+ license : makeLicense ( { status : 'active' , lastSyncErrorCode : null } ) ,
467+ } ) ) ;
468+ expect ( result ?. id ) . not . toBe ( 'licenseReboundElsewhere' ) ;
469+ } ) ;
470+
471+ test ( 'other lastSyncErrorCode does not fire rebound banner' , ( ) => {
472+ const result = resolveActiveBanner ( makeContext ( {
473+ license : makeLicense ( {
474+ status : 'active' ,
475+ lastSyncErrorCode : 'UNKNOWN_STRIPE_PRODUCT' ,
476+ } ) ,
477+ } ) ) ;
478+ expect ( result ?. id ) . not . toBe ( 'licenseReboundElsewhere' ) ;
479+ } ) ;
480+
481+ test ( 'offline license suppresses rebound banner' , ( ) => {
482+ const result = resolveActiveBanner ( makeContext ( {
483+ offlineLicense : makeOfflineLicense ( ) ,
484+ license : makeLicense ( {
485+ status : 'active' ,
486+ lastSyncErrorCode : 'ACTIVATION_CODE_BOUND_TO_DIFFERENT_INSTANCE' ,
487+ } ) ,
488+ } ) ) ;
489+ expect ( result ) . toBeNull ( ) ;
490+ } ) ;
491+
492+ test ( 'rebound banner shown to non-owners' , ( ) => {
493+ // This is a hard lockout — everyone sees it.
494+ const result = resolveActiveBanner ( makeContext ( {
495+ role : OrgRole . MEMBER ,
496+ license : makeLicense ( {
497+ status : 'active' ,
498+ lastSyncErrorCode : 'ACTIVATION_CODE_BOUND_TO_DIFFERENT_INSTANCE' ,
499+ } ) ,
500+ } ) ) ;
501+ expect ( result ?. id ) . toBe ( 'licenseReboundElsewhere' ) ;
502+ } ) ;
503+
504+ test ( 'rebound outranks enforced ping staleness' , ( ) => {
505+ const result = resolveActiveBanner ( makeContext ( {
506+ license : makeLicense ( {
507+ status : 'active' ,
508+ lastSyncAt : new Date ( NOW . getTime ( ) - 14 * 24 * 60 * 60 * 1000 ) ,
509+ lastSyncErrorCode : 'ACTIVATION_CODE_BOUND_TO_DIFFERENT_INSTANCE' ,
510+ } ) ,
511+ } ) ) ;
512+ expect ( result ?. id ) . toBe ( 'licenseReboundElsewhere' ) ;
513+ } ) ;
514+
515+ test ( 'license expired outranks rebound' , ( ) => {
516+ const result = resolveActiveBanner ( makeContext ( {
517+ license : makeLicense ( {
518+ status : 'canceled' ,
519+ lastSyncErrorCode : 'ACTIVATION_CODE_BOUND_TO_DIFFERENT_INSTANCE' ,
520+ } ) ,
521+ } ) ) ;
522+ expect ( result ?. id ) . toBe ( 'licenseExpired' ) ;
523+ } ) ;
524+ } ) ;
525+
450526 describe ( 'trial' , ( ) => {
451527 test ( 'status trialing + future trialEnd → trial banner' , ( ) => {
452528 const result = resolveActiveBanner ( makeContext ( {
0 commit comments