@@ -740,6 +740,195 @@ describe('Enterprise creation invitations', () => {
740740 } )
741741 } )
742742
743+ it . each ( [ 'admin' , 'owner' ] as const ) (
744+ 'recognizes inherited organization %s access without explicit workspace grants' ,
745+ async ( role ) => {
746+ const payload = operationPayload ( {
747+ request : {
748+ ...operationPayload ( ) . request ,
749+ workspaceIds : [ 'workspace-1' , 'workspace-2' ] ,
750+ } ,
751+ applicationResult : {
752+ appliedAt : '2026-08-13T00:00:00.000Z' ,
753+ subscriptionId : 'sub-1' ,
754+ } ,
755+ } )
756+ queueTableRows ( schemaMock . outboxEvent , [
757+ { eventType : 'stripe.provision-enterprise' , payload } ,
758+ ] )
759+ queueTableRows ( schemaMock . outboxEvent , [ ] )
760+ queueTableRows ( schemaMock . outboxEvent , [ { status : 'completed' } , { status : 'completed' } ] )
761+ queueTableRows (
762+ schemaMock . user ,
763+ [ 'workspace-1' , 'workspace-2' ] . map ( ( workspaceId ) => ( {
764+ userId : 'invitee-1' ,
765+ workspaceId,
766+ role,
767+ permission : null ,
768+ } ) )
769+ )
770+ const checkpointPayload = vi . fn ( )
771+
772+ await inviteEnterprisePeople (
773+ {
774+ provisioningOperationId : 'operation-1' ,
775+ organizationId : 'org-1' ,
776+ ownerUserId : 'owner-1' ,
777+ email : 'new@example.com' ,
778+ role : 'admin' ,
779+ permission : 'admin' ,
780+ sequence : 0 ,
781+ } ,
782+ {
783+ eventId : 'invite-1' ,
784+ eventType : 'enterprise.invite-people' ,
785+ attempts : 0 ,
786+ checkpointPayload,
787+ }
788+ )
789+
790+ expect ( checkpointPayload ) . toHaveBeenCalledExactlyOnceWith ( {
791+ delivery : {
792+ completedAt : expect . any ( String ) ,
793+ resultId : 'invitee-1' ,
794+ outcome : 'unchanged' ,
795+ } ,
796+ } )
797+ expect ( mocks . createWorkspaceInvitation ) . not . toHaveBeenCalled ( )
798+ expect ( mocks . prepareWorkspaceInvitationContext ) . not . toHaveBeenCalled ( )
799+ expect ( mocks . sendInvitationEmail ) . not . toHaveBeenCalled ( )
800+ }
801+ )
802+
803+ it . each ( [
804+ {
805+ name : 'a concurrent promotion' ,
806+ role : 'admin' ,
807+ permission : null ,
808+ requestedRole : 'member' ,
809+ workspaceIds : [ 'workspace-1' ] ,
810+ applied : true ,
811+ } ,
812+ {
813+ name : 'a sufficient explicit grant' ,
814+ role : 'member' ,
815+ permission : 'write' ,
816+ requestedRole : 'member' ,
817+ workspaceIds : [ 'workspace-1' ] ,
818+ applied : true ,
819+ } ,
820+ {
821+ name : 'an insufficient explicit grant' ,
822+ role : 'member' ,
823+ permission : 'read' ,
824+ requestedRole : 'member' ,
825+ workspaceIds : [ 'workspace-1' ] ,
826+ applied : false ,
827+ } ,
828+ {
829+ name : 'a workspace leaving the organization scope' ,
830+ role : null ,
831+ permission : null ,
832+ requestedRole : 'member' ,
833+ workspaceIds : [ 'workspace-1' ] ,
834+ applied : false ,
835+ } ,
836+ {
837+ name : 'a workspace admin grant without the requested organization admin role' ,
838+ role : 'member' ,
839+ permission : 'admin' ,
840+ requestedRole : 'admin' ,
841+ workspaceIds : [ 'workspace-1' ] ,
842+ applied : false ,
843+ } ,
844+ {
845+ name : 'inherited access to only one of two requested workspaces' ,
846+ role : 'admin' ,
847+ permission : null ,
848+ requestedRole : 'member' ,
849+ workspaceIds : [ 'workspace-1' , 'workspace-2' ] ,
850+ applied : false ,
851+ } ,
852+ ] as const ) (
853+ 'checks the final effective access after $name' ,
854+ async ( { role, permission, requestedRole, workspaceIds, applied } ) => {
855+ const payload = operationPayload ( {
856+ request : { ...operationPayload ( ) . request , workspaceIds : [ ...workspaceIds ] } ,
857+ applicationResult : {
858+ appliedAt : '2026-08-13T00:00:00.000Z' ,
859+ subscriptionId : 'sub-1' ,
860+ } ,
861+ } )
862+ queueTableRows ( schemaMock . outboxEvent , [
863+ { eventType : 'stripe.provision-enterprise' , payload } ,
864+ ] )
865+ queueTableRows ( schemaMock . outboxEvent , [ ] )
866+ queueTableRows (
867+ schemaMock . outboxEvent ,
868+ workspaceIds . map ( ( ) => ( { status : 'completed' } ) )
869+ )
870+ queueTableRows ( schemaMock . user , [
871+ { userId : 'invitee-1' , workspaceId : 'workspace-1' , role : 'member' , permission : null } ,
872+ ] )
873+ queueTableRows ( schemaMock . invitation , [ ] )
874+ queueTableRows ( schemaMock . user , [ { organizationId : 'org-1' } ] )
875+ queueTableRows ( schemaMock . user , [
876+ { id : 'owner-1' , name : 'Owner' , email : 'owner@example.com' } ,
877+ ] )
878+ queueTableRows (
879+ schemaMock . user ,
880+ role ? [ { userId : 'invitee-1' , workspaceId : 'workspace-1' , role, permission } ] : [ ]
881+ )
882+ queueTableRows ( schemaMock . invitation , [ ] )
883+ mocks . createWorkspaceInvitation . mockResolvedValueOnce ( {
884+ id : 'invitee-1' ,
885+ instantAdd : true ,
886+ outcome : 'unchanged' ,
887+ workspaceIds : [ ] ,
888+ } )
889+ const checkpointPayload = vi . fn ( )
890+ const result = inviteEnterprisePeople (
891+ {
892+ provisioningOperationId : 'operation-1' ,
893+ organizationId : 'org-1' ,
894+ ownerUserId : 'owner-1' ,
895+ email : 'new@example.com' ,
896+ role : requestedRole ,
897+ permission : 'write' ,
898+ sequence : 0 ,
899+ } ,
900+ {
901+ eventId : 'invite-1' ,
902+ eventType : 'enterprise.invite-people' ,
903+ attempts : 0 ,
904+ checkpointPayload,
905+ }
906+ )
907+
908+ if ( applied ) {
909+ await expect ( result ) . resolves . toBeUndefined ( )
910+ expect ( checkpointPayload ) . toHaveBeenLastCalledWith ( {
911+ delivery : {
912+ completedAt : expect . any ( String ) ,
913+ resultId : 'invitee-1' ,
914+ outcome : 'unchanged' ,
915+ } ,
916+ } )
917+ } else {
918+ await expect ( result ) . rejects . toThrow (
919+ 'did not apply the requested organization role and workspace permissions'
920+ )
921+ expect ( checkpointPayload ) . toHaveBeenCalledExactlyOnceWith ( {
922+ attemptedAt : expect . any ( String ) ,
923+ } )
924+ }
925+ expect ( mocks . createWorkspaceInvitation ) . toHaveBeenCalledExactlyOnceWith (
926+ expect . objectContaining ( { existingAccessPolicy : 'ensure-at-least' } )
927+ )
928+ expect ( mocks . sendInvitationEmail ) . not . toHaveBeenCalled ( )
929+ }
930+ )
931+
743932 it ( 'waits without consuming attempts until every selected workspace move completes' , async ( ) => {
744933 const payload = operationPayload ( {
745934 request : {
0 commit comments