@@ -9,14 +9,15 @@ import {
99getScenarioData ,
1010getMockBookingAttendee ,
1111getDate ,
12+ mockCalendar ,
1213} from "@calcom/web/test/utils/bookingScenario/bookingScenario" ;
1314import { expectBookingRequestRescheduledEmails } from "@calcom/web/test/utils/bookingScenario/expects" ;
1415
1516import type { Request , Response } from "express" ;
1617import type { NextApiRequest , NextApiResponse } from "next" ;
17- import { describe } from "vitest" ;
18+ import { describe , expect } from "vitest" ;
1819
19- import { SchedulingType } from "@calcom/prisma/enums" ;
20+ import { SchedulingType , MembershipRole } from "@calcom/prisma/enums" ;
2021import { BookingStatus } from "@calcom/prisma/enums" ;
2122import type { TRequestRescheduleInputSchema } from "@calcom/trpc/server/routers/viewer/bookings/requestReschedule.schema" ;
2223import type { TrpcSessionUser } from "@calcom/trpc/server/types" ;
@@ -253,6 +254,297 @@ describe("Handler: requestReschedule", () => {
253254bookNewTimePath :"/team/team-1/event-type-1" ,
254255} ) ;
255256} ) ;
257+
258+ test ( `should allow team admin to request-reschedule for a team booking and use organizer's credentials
259+ 1. Team admin (non-organizer) can request reschedule with proper permissions
260+ 2. Organizer's credentials are used to delete calendar events` , async ( { emails} ) => {
261+ const { requestRescheduleHandler} = await import (
262+ "@calcom/trpc/server/routers/viewer/bookings/requestReschedule.handler"
263+ ) ;
264+
265+ const booker = getBooker ( {
266+ email :"booker@example.com" ,
267+ name :"Booker" ,
268+ } ) ;
269+
270+ const organizer = getOrganizer ( {
271+ name :"Organizer" ,
272+ email :"organizer@example.com" ,
273+ id :101 ,
274+ teams :[
275+ {
276+ membership :{
277+ accepted :true ,
278+ role :"MEMBER" ,
279+ } ,
280+ team :{
281+ id :1 ,
282+ name :"Team 1" ,
283+ slug :"team-1" ,
284+ } ,
285+ } ,
286+ ] ,
287+ schedules :[ TestData . schedules . IstWorkHours ] ,
288+ credentials :[ getGoogleCalendarCredential ( ) ] ,
289+ selectedCalendars :[ TestData . selectedCalendars . google ] ,
290+ } ) ;
291+
292+ const teamAdmin = {
293+ id :102 ,
294+ username :"team-admin" ,
295+ name :"Team Admin" ,
296+ email :"team-admin@example.com" ,
297+ locale :"en" ,
298+ timeZone :"America/New_York" ,
299+ teams :[
300+ {
301+ membership :{
302+ accepted :true ,
303+ role :MembershipRole . ADMIN ,
304+ } ,
305+ team :{
306+ id :1 ,
307+ name :"Team 1" ,
308+ slug :"team-1" ,
309+ } ,
310+ } ,
311+ ] ,
312+ schedules :[ TestData . schedules . IstWorkHours ] ,
313+ credentials :[ ] , // No credentials
314+ selectedCalendars :[ ] ,
315+ } ;
316+
317+ const { dateString :plus1DateString } = getDate ( { dateIncrement :1 } ) ;
318+ const bookingUid = "MOCKED_BOOKING_UID_TEAM_ADMIN" ;
319+ const eventTypeSlug = "event-type-1" ;
320+
321+ const calendarMock = await mockCalendar ( "googlecalendar" ) ;
322+
323+ await createBookingScenario (
324+ getScenarioData ( {
325+ webhooks :[
326+ {
327+ userId :organizer . id ,
328+ eventTriggers :[ "BOOKING_CREATED" ] ,
329+ subscriberUrl :"http://my-webhook.example.com" ,
330+ active :true ,
331+ eventTypeId :1 ,
332+ appId :null ,
333+ } ,
334+ ] ,
335+ eventTypes :[
336+ {
337+ id :1 ,
338+ slug :eventTypeSlug ,
339+ slotInterval :45 ,
340+ teamId :1 ,
341+ schedulingType :SchedulingType . COLLECTIVE ,
342+ length :45 ,
343+ users :[
344+ {
345+ id :101 ,
346+ } ,
347+ ] ,
348+ } ,
349+ ] ,
350+ bookings :[
351+ {
352+ uid :bookingUid ,
353+ eventTypeId :1 ,
354+ userId :101 , // Booking belongs to organizer
355+ status :BookingStatus . ACCEPTED ,
356+ startTime :`${ plus1DateString } T05:00:00.000Z` ,
357+ endTime :`${ plus1DateString } T05:15:00.000Z` ,
358+ references :[
359+ {
360+ type :"google_calendar" ,
361+ uid :"MOCK_CALENDAR_EVENT_UID" ,
362+ meetingId :"MOCK_MEETING_ID" ,
363+ meetingPassword :"MOCK_PASSWORD" ,
364+ meetingUrl :"https://UNUSED_URL" ,
365+ credentialId :1 ,
366+ } ,
367+ ] ,
368+ attendees :[
369+ getMockBookingAttendee ( {
370+ id :2 ,
371+ name :booker . name ,
372+ email :booker . email ,
373+ locale :"hi" ,
374+ timeZone :"Asia/Kolkata" ,
375+ noShow :false ,
376+ } ) ,
377+ ] ,
378+ } ,
379+ ] ,
380+ organizer,
381+ usersApartFromOrganizer :[ teamAdmin ] ,
382+ apps :[ TestData . apps [ "google-calendar" ] , TestData . apps [ "daily-video" ] ] ,
383+ } )
384+ ) ;
385+
386+ const loggedInTeamAdmin = {
387+ organizationId :null ,
388+ id :102 , // Team admin ID
389+ username :"team-admin" ,
390+ name :"Team Admin" ,
391+ email :"team-admin@example.com" ,
392+ } ;
393+
394+ await requestRescheduleHandler (
395+ getTrpcHandlerData ( {
396+ user :loggedInTeamAdmin ,
397+ input :{
398+ bookingUid,
399+ rescheduleReason :"Team admin requesting reschedule" ,
400+ } ,
401+ } )
402+ ) ;
403+
404+ expectBookingRequestRescheduledEmails ( {
405+ booking :{
406+ uid :bookingUid ,
407+ } ,
408+ booker,
409+ organizer :organizer ,
410+ loggedInUser :loggedInTeamAdmin ,
411+ emails,
412+ bookNewTimePath :"/team/team-1/event-type-1" ,
413+ } ) ;
414+
415+ const deleteEventCalls = calendarMock . deleteEventCalls ;
416+ expect ( deleteEventCalls . length ) . toBe ( 1 ) ;
417+
418+ const credentialUsed = deleteEventCalls [ 0 ] . calendarServiceConstructorArgs . credential ;
419+ expect ( credentialUsed . userId ) . toBe ( organizer . id ) ;
420+ expect ( credentialUsed . id ) . toBe ( 1 ) ;
421+ } ) ;
422+
423+ test ( `should reject request-reschedule from team member without proper permissions` , async ( ) => {
424+ const { requestRescheduleHandler} = await import (
425+ "@calcom/trpc/server/routers/viewer/bookings/requestReschedule.handler"
426+ ) ;
427+
428+ const booker = getBooker ( {
429+ email :"booker@example.com" ,
430+ name :"Booker" ,
431+ } ) ;
432+
433+ const organizer = getOrganizer ( {
434+ name :"Organizer" ,
435+ email :"organizer@example.com" ,
436+ id :101 ,
437+ teams :[
438+ {
439+ membership :{
440+ accepted :true ,
441+ role :"MEMBER" ,
442+ } ,
443+ team :{
444+ id :1 ,
445+ name :"Team 1" ,
446+ slug :"team-1" ,
447+ } ,
448+ } ,
449+ ] ,
450+ schedules :[ TestData . schedules . IstWorkHours ] ,
451+ credentials :[ getGoogleCalendarCredential ( ) ] ,
452+ selectedCalendars :[ TestData . selectedCalendars . google ] ,
453+ } ) ;
454+
455+ const teamMember = {
456+ id :103 ,
457+ username :"team-member" ,
458+ name :"Team Member" ,
459+ email :"team-member@example.com" ,
460+ locale :"en" ,
461+ timeZone :"America/New_York" ,
462+ teams :[
463+ {
464+ membership :{
465+ accepted :true ,
466+ role :MembershipRole . MEMBER ,
467+ } ,
468+ team :{
469+ id :1 ,
470+ name :"Team 1" ,
471+ slug :"team-1" ,
472+ } ,
473+ } ,
474+ ] ,
475+ schedules :[ TestData . schedules . IstWorkHours ] ,
476+ credentials :[ ] ,
477+ selectedCalendars :[ ] ,
478+ } ;
479+
480+ const { dateString :plus1DateString } = getDate ( { dateIncrement :1 } ) ;
481+ const bookingUid = "MOCKED_BOOKING_UID_MEMBER" ;
482+ const eventTypeSlug = "event-type-1" ;
483+
484+ await createBookingScenario (
485+ getScenarioData ( {
486+ eventTypes :[
487+ {
488+ id :1 ,
489+ slug :eventTypeSlug ,
490+ slotInterval :45 ,
491+ teamId :1 ,
492+ schedulingType :SchedulingType . COLLECTIVE ,
493+ length :45 ,
494+ users :[
495+ {
496+ id :101 ,
497+ } ,
498+ ] ,
499+ } ,
500+ ] ,
501+ bookings :[
502+ {
503+ uid :bookingUid ,
504+ eventTypeId :1 ,
505+ userId :101 , // Booking belongs to organizer
506+ status :BookingStatus . ACCEPTED ,
507+ startTime :`${ plus1DateString } T05:00:00.000Z` ,
508+ endTime :`${ plus1DateString } T05:15:00.000Z` ,
509+ attendees :[
510+ getMockBookingAttendee ( {
511+ id :2 ,
512+ name :booker . name ,
513+ email :booker . email ,
514+ locale :"hi" ,
515+ timeZone :"Asia/Kolkata" ,
516+ noShow :false ,
517+ } ) ,
518+ ] ,
519+ } ,
520+ ] ,
521+ organizer,
522+ usersApartFromOrganizer :[ teamMember ] ,
523+ apps :[ TestData . apps [ "google-calendar" ] , TestData . apps [ "daily-video" ] ] ,
524+ } )
525+ ) ;
526+
527+ const loggedInTeamMember = {
528+ organizationId :null ,
529+ id :103 , // Team member ID
530+ username :"team-member" ,
531+ name :"Team Member" ,
532+ email :"team-member@example.com" ,
533+ } ;
534+
535+ await expect (
536+ requestRescheduleHandler (
537+ getTrpcHandlerData ( {
538+ user :loggedInTeamMember ,
539+ input :{
540+ bookingUid,
541+ rescheduleReason :"Team member trying to reschedule" ,
542+ } ,
543+ } )
544+ )
545+ ) . rejects . toThrow ( "User does not have permission to request reschedule for this booking" ) ;
546+ } ) ;
547+
256548test . todo ( "Verify that the email should go to organizer as well as the team members" ) ;
257549} ) ;
258550} ) ;