@@ -302,6 +302,47 @@ describe('PricingTable - plans visibility', () => {
302302 pathRoot : '' ,
303303 reload : vi . fn ( ) ,
304304 } as const ;
305+ const seatLimitedOrganizationPlan = {
306+ ...testPlan ,
307+ id : 'plan_org_target' ,
308+ name : 'Organization Plan' ,
309+ slug : 'organization-plan' ,
310+ forPayerType : 'org' ,
311+ unitPrices : [
312+ {
313+ name : 'seats' ,
314+ blockSize : 1 ,
315+ tiers : [
316+ {
317+ id : 'tier_org_target_1' ,
318+ startsAtBlock : 1 ,
319+ endsAfterBlock : 20 ,
320+ feePerBlock : testPlan . fee ,
321+ } ,
322+ ] ,
323+ } ,
324+ ] ,
325+ } as const ;
326+ const currentOrganizationPlan = {
327+ ...seatLimitedOrganizationPlan ,
328+ id : 'plan_org_current' ,
329+ name : 'Current Organization Plan' ,
330+ slug : 'current-organization-plan' ,
331+ unitPrices : [
332+ {
333+ name : 'seats' ,
334+ blockSize : 1 ,
335+ tiers : [
336+ {
337+ id : 'tier_org_current_1' ,
338+ startsAtBlock : 1 ,
339+ endsAfterBlock : 50 ,
340+ feePerBlock : testPlan . fee ,
341+ } ,
342+ ] ,
343+ } ,
344+ ] ,
345+ } as const ;
305346
306347 it ( 'shows no plans when user is signed in but has no subscription' , async ( ) => {
307348 const { wrapper, fixtures, props } = await createFixtures ( f => {
@@ -581,6 +622,128 @@ describe('PricingTable - plans visibility', () => {
581622 } ) ;
582623 } ) ;
583624
625+ it ( 'disables switching to an organization plan when the active org exceeds its seat limit' , async ( ) => {
626+ const { wrapper, fixtures, props } = await createFixtures ( f => {
627+ f . withBilling ( ) ;
628+ f . withOrganizations ( ) ;
629+ f . withUser ( {
630+ email_addresses : [ 'test@clerk.com' ] ,
631+ organization_memberships : [
632+ {
633+ name : 'Org1' ,
634+ permissions : [ 'org:sys_billing:manage' ] ,
635+ members_count : 17 ,
636+ pending_invitations_count : 4 ,
637+ } ,
638+ ] ,
639+ } ) ;
640+ } ) ;
641+
642+ props . setProps ( { for : 'organization' } as any ) ;
643+
644+ fixtures . clerk . billing . getStatements . mockRejectedValue ( ) ;
645+ fixtures . clerk . organization . getPaymentMethods . mockRejectedValue ( ) ;
646+ fixtures . clerk . billing . getPlans . mockResolvedValue ( { data : [ seatLimitedOrganizationPlan as any ] , total_count : 1 } ) ;
647+ fixtures . clerk . billing . getSubscription . mockResolvedValue ( {
648+ id : 'sub_org_active' ,
649+ status : 'active' ,
650+ activeAt : new Date ( '2021-01-01' ) ,
651+ createdAt : new Date ( '2021-01-01' ) ,
652+ nextPayment : null ,
653+ pastDueAt : null ,
654+ updatedAt : null ,
655+ subscriptionItems : [
656+ {
657+ id : 'si_org_active' ,
658+ plan : currentOrganizationPlan ,
659+ createdAt : new Date ( '2021-01-01' ) ,
660+ paymentMethodId : 'src_1' ,
661+ pastDueAt : null ,
662+ canceledAt : null ,
663+ periodStart : new Date ( '2021-01-01' ) ,
664+ periodEnd : new Date ( '2021-01-31' ) ,
665+ planPeriod : 'month' as const ,
666+ status : 'active' as const ,
667+ isFreeTrial : false ,
668+ cancel : vi . fn ( ) ,
669+ pathRoot : '' ,
670+ reload : vi . fn ( ) ,
671+ } ,
672+ ] ,
673+ pathRoot : '' ,
674+ reload : vi . fn ( ) ,
675+ } ) ;
676+
677+ const { getByRole } = render ( < PricingTable /> , { wrapper } ) ;
678+
679+ await waitFor ( ( ) => {
680+ expect ( getByRole ( 'heading' , { name : 'Organization Plan' } ) ) . toBeVisible ( ) ;
681+ } ) ;
682+
683+ expect ( getByRole ( 'button' , { name : 'Switch to this plan' } ) ) . toBeDisabled ( ) ;
684+ } ) ;
685+
686+ it ( 'keeps switching enabled when the active org is exactly at the plan seat limit' , async ( ) => {
687+ const { wrapper, fixtures, props } = await createFixtures ( f => {
688+ f . withBilling ( ) ;
689+ f . withOrganizations ( ) ;
690+ f . withUser ( {
691+ email_addresses : [ 'test@clerk.com' ] ,
692+ organization_memberships : [
693+ {
694+ name : 'Org1' ,
695+ permissions : [ 'org:sys_billing:manage' ] ,
696+ members_count : 17 ,
697+ pending_invitations_count : 3 ,
698+ } ,
699+ ] ,
700+ } ) ;
701+ } ) ;
702+
703+ props . setProps ( { for : 'organization' } as any ) ;
704+
705+ fixtures . clerk . billing . getStatements . mockRejectedValue ( ) ;
706+ fixtures . clerk . organization . getPaymentMethods . mockRejectedValue ( ) ;
707+ fixtures . clerk . billing . getPlans . mockResolvedValue ( { data : [ seatLimitedOrganizationPlan as any ] , total_count : 1 } ) ;
708+ fixtures . clerk . billing . getSubscription . mockResolvedValue ( {
709+ id : 'sub_org_active' ,
710+ status : 'active' ,
711+ activeAt : new Date ( '2021-01-01' ) ,
712+ createdAt : new Date ( '2021-01-01' ) ,
713+ nextPayment : null ,
714+ pastDueAt : null ,
715+ updatedAt : null ,
716+ subscriptionItems : [
717+ {
718+ id : 'si_org_active' ,
719+ plan : currentOrganizationPlan ,
720+ createdAt : new Date ( '2021-01-01' ) ,
721+ paymentMethodId : 'src_1' ,
722+ pastDueAt : null ,
723+ canceledAt : null ,
724+ periodStart : new Date ( '2021-01-01' ) ,
725+ periodEnd : new Date ( '2021-01-31' ) ,
726+ planPeriod : 'month' as const ,
727+ status : 'active' as const ,
728+ isFreeTrial : false ,
729+ cancel : vi . fn ( ) ,
730+ pathRoot : '' ,
731+ reload : vi . fn ( ) ,
732+ } ,
733+ ] ,
734+ pathRoot : '' ,
735+ reload : vi . fn ( ) ,
736+ } ) ;
737+
738+ const { getByRole } = render ( < PricingTable /> , { wrapper } ) ;
739+
740+ await waitFor ( ( ) => {
741+ expect ( getByRole ( 'heading' , { name : 'Organization Plan' } ) ) . toBeVisible ( ) ;
742+ } ) ;
743+
744+ expect ( getByRole ( 'button' , { name : 'Switch to this plan' } ) ) . toBeEnabled ( ) ;
745+ } ) ;
746+
584747 it ( 'fetches user plans and renders when using for: user' , async ( ) => {
585748 const { wrapper, fixtures, props } = await createFixtures ( f => {
586749 f . withUser ( { email_addresses : [ 'test@clerk.com' ] } ) ;
0 commit comments