@@ -193,7 +193,17 @@ export class PeopleInviteService {
193193 data : { deactivated : false , isActive : true , role : roleString } ,
194194 } ) ;
195195 } else {
196- member = existingMember ;
196+ // Active member re-added: union the new roles into their existing roles
197+ // so we never strip a role they already have, and so adding a role
198+ // actually takes effect instead of silently no-op'ing.
199+ const mergedRole = this . mergeRoleString ( existingMember . role , roles ) ;
200+ member =
201+ mergedRole === this . normalizeRoleString ( existingMember . role )
202+ ? existingMember
203+ : await db . member . update ( {
204+ where : { id : existingMember . id } ,
205+ data : { role : mergedRole } ,
206+ } ) ;
197207 }
198208 } else {
199209 member = await db . member . create ( {
@@ -276,14 +286,18 @@ export class PeopleInviteService {
276286 return ;
277287 }
278288
279- await this . sendInvitationEmailToExistingMember ( {
280- email,
281- roles,
282- organizationId,
283- inviterId : currentUserId ,
284- sendPortalEmail,
285- sendAppEmail,
286- } ) ;
289+ // Already an active member: an invitation/accept round-trip can't grant
290+ // new roles to someone who is already in the org (and historically left
291+ // their role unchanged, so promoting an employee to admin silently
292+ // failed and the user hit "Access Denied"). Upgrade their role in place
293+ // by unioning the new roles into their existing roles.
294+ const mergedRole = this . mergeRoleString ( existingMember . role , roles ) ;
295+ if ( mergedRole !== this . normalizeRoleString ( existingMember . role ) ) {
296+ await db . member . update ( {
297+ where : { id : existingMember . id } ,
298+ data : { role : mergedRole } ,
299+ } ) ;
300+ }
287301 return ;
288302 }
289303 }
@@ -319,51 +333,36 @@ export class PeopleInviteService {
319333 } ) ;
320334 }
321335
322- private async sendInvitationEmailToExistingMember ( params : {
323- email : string ;
324- roles : string [ ] ;
325- organizationId : string ;
326- inviterId : string ;
327- sendPortalEmail ?: boolean ;
328- sendAppEmail ?: boolean ;
329- } ) : Promise < void > {
330- const {
331- email,
332- roles,
333- organizationId,
334- inviterId,
335- sendPortalEmail,
336- sendAppEmail,
337- } = params ;
338-
339- const organization = await db . organization . findUnique ( {
340- where : { id : organizationId } ,
341- select : { name : true } ,
342- } ) ;
343-
344- if ( ! organization ) {
345- throw new BadRequestException ( 'Organization not found.' ) ;
346- }
347-
348- const invitation = await db . invitation . create ( {
349- data : {
350- email : email . toLowerCase ( ) ,
351- organizationId,
352- role : roles . length === 1 ? roles [ 0 ] : roles . join ( ',' ) ,
353- status : 'pending' ,
354- expiresAt : new Date ( Date . now ( ) + 7 * 24 * 60 * 60 * 1000 ) ,
355- inviterId,
356- } ,
357- } ) ;
336+ /** Sort + de-dupe a comma-separated role string into a canonical form. */
337+ private normalizeRoleString ( role : string | null | undefined ) : string {
338+ return [
339+ ...new Set (
340+ ( role ?? '' )
341+ . split ( ',' )
342+ . map ( ( r ) => r . trim ( ) )
343+ . filter ( Boolean ) ,
344+ ) ,
345+ ]
346+ . sort ( )
347+ . join ( ',' ) ;
348+ }
358349
359- await this . sendInviteEmails ( {
360- email : email . toLowerCase ( ) ,
361- organizationName : organization . name ,
362- sendPortalEmail,
363- sendAppEmail,
364- portalLink : this . buildPortalUrl ( organizationId ) ,
365- appLink : this . buildInviteLink ( invitation . id ) ,
366- } ) ;
350+ /** Union new roles into an existing comma-separated role string. */
351+ private mergeRoleString (
352+ existingRole : string | null | undefined ,
353+ addedRoles : string [ ] ,
354+ ) : string {
355+ return [
356+ ...new Set ( [
357+ ...( existingRole ?? '' )
358+ . split ( ',' )
359+ . map ( ( r ) => r . trim ( ) )
360+ . filter ( Boolean ) ,
361+ ...addedRoles . map ( ( r ) => r . trim ( ) ) . filter ( Boolean ) ,
362+ ] ) ,
363+ ]
364+ . sort ( )
365+ . join ( ',' ) ;
367366 }
368367
369368 async resendPortalInvite ( params : {
0 commit comments