@@ -59,8 +59,12 @@ const mockWorkflowReminderRepository: Pick<WorkflowReminderRepository, "findById
5959 findByIdIncludeStepAndWorkflow : vi . fn ( ) ,
6060} ;
6161
62- const mockBookingSeatRepository : Pick < BookingSeatRepository , "getByUidIncludeAttendee" > = {
62+ const mockBookingSeatRepository : Pick <
63+ BookingSeatRepository ,
64+ "getByUidIncludeAttendee" | "getByReferenceUidWithAttendeeDetails"
65+ > = {
6366 getByUidIncludeAttendee : vi . fn ( ) ,
67+ getByReferenceUidWithAttendeeDetails : vi . fn ( ) ,
6468} ;
6569
6670describe ( "EmailWorkflowService" , ( ) => {
@@ -605,6 +609,345 @@ describe("EmailWorkflowService", () => {
605609 } ) ;
606610 } ) ;
607611
612+ describe ( "generateEmailPayloadForEvtWorkflow - Platform URL handling" , ( ) => {
613+ const baseBookingInfo = {
614+ uid : "booking-123" ,
615+ bookerUrl : "https://cal.com" ,
616+ title : "Test Meeting" ,
617+ startTime : "2024-12-01T10:00:00Z" ,
618+ endTime : "2024-12-01T11:00:00Z" ,
619+ organizer : {
620+ name : "Organizer Name" ,
621+ email : "organizer@example.com" ,
622+ timeZone : "UTC" ,
623+ language : { locale : "en" } ,
624+ timeFormat : "h:mma" ,
625+ username : "organizer-user" ,
626+ } ,
627+ attendees : [
628+ {
629+ name : "Attendee Name" ,
630+ email : "attendee@example.com" ,
631+ timeZone : "UTC" ,
632+ language : { locale : "en" } ,
633+ } ,
634+ ] ,
635+ eventType : {
636+ slug : "test-event" ,
637+ recurringEvent : null ,
638+ } ,
639+ } ;
640+
641+ test ( "should use platform cancel URL when platformClientId and platformCancelUrl are provided" , async ( ) => {
642+ const mockBookingInfoWithPlatform = {
643+ ...baseBookingInfo ,
644+ platformClientId : "platform-client-123" ,
645+ platformCancelUrl : "https://platform.example.com/cancel" ,
646+ platformRescheduleUrl : "https://platform.example.com/reschedule" ,
647+ } ;
648+
649+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
650+ evt : mockBookingInfoWithPlatform ,
651+ sendTo : [ "attendee@example.com" ] ,
652+ hideBranding : false ,
653+ emailSubject : "Test Subject" ,
654+ emailBody : "Cancel here: {CANCEL_URL}" ,
655+ sender : "Cal.com" ,
656+ action : WorkflowActions . EMAIL_ATTENDEE ,
657+ template : WorkflowTemplates . CUSTOM ,
658+ includeCalendarEvent : false ,
659+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
660+ } ) ;
661+
662+ // The cancel link should use the platform URL
663+ expect ( result . html ) . toContain ( "https://platform.example.com/cancel/booking-123" ) ;
664+ expect ( result . html ) . toContain ( "slug=test-event" ) ;
665+ expect ( result . html ) . toContain ( "username=organizer-user" ) ;
666+ expect ( result . html ) . toContain ( "cancel=true" ) ;
667+ } ) ;
668+
669+ test ( "should use platform reschedule URL when platformClientId and platformRescheduleUrl are provided" , async ( ) => {
670+ const mockBookingInfoWithPlatform = {
671+ ...baseBookingInfo ,
672+ platformClientId : "platform-client-123" ,
673+ platformCancelUrl : "https://platform.example.com/cancel" ,
674+ platformRescheduleUrl : "https://platform.example.com/reschedule" ,
675+ } ;
676+
677+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
678+ evt : mockBookingInfoWithPlatform ,
679+ sendTo : [ "attendee@example.com" ] ,
680+ hideBranding : false ,
681+ emailSubject : "Test Subject" ,
682+ emailBody : "Reschedule here: {RESCHEDULE_URL}" ,
683+ sender : "Cal.com" ,
684+ action : WorkflowActions . EMAIL_ATTENDEE ,
685+ template : WorkflowTemplates . CUSTOM ,
686+ includeCalendarEvent : false ,
687+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
688+ } ) ;
689+
690+ // The reschedule link should use the platform URL
691+ expect ( result . html ) . toContain ( "https://platform.example.com/reschedule/booking-123" ) ;
692+ expect ( result . html ) . toContain ( "slug=test-event" ) ;
693+ expect ( result . html ) . toContain ( "username=organizer-user" ) ;
694+ expect ( result . html ) . toContain ( "reschedule=true" ) ;
695+ } ) ;
696+
697+ test ( "should use standard bookerUrl when platformClientId is not provided" , async ( ) => {
698+ const mockBookingInfoWithoutPlatform = {
699+ ...baseBookingInfo ,
700+ platformClientId : null ,
701+ platformCancelUrl : null ,
702+ platformRescheduleUrl : null ,
703+ } ;
704+
705+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
706+ evt : mockBookingInfoWithoutPlatform ,
707+ sendTo : [ "attendee@example.com" ] ,
708+ hideBranding : false ,
709+ emailSubject : "Test Subject" ,
710+ emailBody : "Cancel: {CANCEL_URL} Reschedule: {RESCHEDULE_URL}" ,
711+ sender : "Cal.com" ,
712+ action : WorkflowActions . EMAIL_ATTENDEE ,
713+ template : WorkflowTemplates . CUSTOM ,
714+ includeCalendarEvent : false ,
715+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
716+ } ) ;
717+
718+ // Should use standard bookerUrl pattern
719+ expect ( result . html ) . toContain ( "https://cal.com/booking/booking-123?cancel=true" ) ;
720+ expect ( result . html ) . toContain ( "https://cal.com/reschedule/booking-123" ) ;
721+ // Should NOT contain platform URLs
722+ expect ( result . html ) . not . toContain ( "platform.example.com" ) ;
723+ } ) ;
724+
725+ test ( "should include teamId in platform cancel URL when team is provided" , async ( ) => {
726+ const mockBookingInfoWithTeam = {
727+ ...baseBookingInfo ,
728+ platformClientId : "platform-client-123" ,
729+ platformCancelUrl : "https://platform.example.com/cancel" ,
730+ platformRescheduleUrl : "https://platform.example.com/reschedule" ,
731+ team : {
732+ id : 42 ,
733+ name : "Test Team" ,
734+ members : [ ] ,
735+ } ,
736+ } ;
737+
738+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
739+ evt : mockBookingInfoWithTeam ,
740+ sendTo : [ "attendee@example.com" ] ,
741+ hideBranding : false ,
742+ emailSubject : "Test Subject" ,
743+ emailBody : "Cancel here: {CANCEL_URL}" ,
744+ sender : "Cal.com" ,
745+ action : WorkflowActions . EMAIL_ATTENDEE ,
746+ template : WorkflowTemplates . CUSTOM ,
747+ includeCalendarEvent : false ,
748+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
749+ } ) ;
750+
751+ expect ( result . html ) . toContain ( "teamId=42" ) ;
752+ } ) ;
753+
754+ test ( "should include teamId in platform reschedule URL when team is provided" , async ( ) => {
755+ const mockBookingInfoWithTeam = {
756+ ...baseBookingInfo ,
757+ platformClientId : "platform-client-123" ,
758+ platformCancelUrl : "https://platform.example.com/cancel" ,
759+ platformRescheduleUrl : "https://platform.example.com/reschedule" ,
760+ team : {
761+ id : 42 ,
762+ name : "Test Team" ,
763+ members : [ ] ,
764+ } ,
765+ } ;
766+
767+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
768+ evt : mockBookingInfoWithTeam ,
769+ sendTo : [ "attendee@example.com" ] ,
770+ hideBranding : false ,
771+ emailSubject : "Test Subject" ,
772+ emailBody : "Reschedule here: {RESCHEDULE_URL}" ,
773+ sender : "Cal.com" ,
774+ action : WorkflowActions . EMAIL_ATTENDEE ,
775+ template : WorkflowTemplates . CUSTOM ,
776+ includeCalendarEvent : false ,
777+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
778+ } ) ;
779+
780+ expect ( result . html ) . toContain ( "teamId=42" ) ;
781+ } ) ;
782+
783+ test ( "should include seatReferenceUid in platform cancel URL for seated events" , async ( ) => {
784+ const mockBookingInfoWithPlatform = {
785+ ...baseBookingInfo ,
786+ platformClientId : "platform-client-123" ,
787+ platformCancelUrl : "https://platform.example.com/cancel" ,
788+ platformRescheduleUrl : "https://platform.example.com/reschedule" ,
789+ } ;
790+
791+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
792+ evt : mockBookingInfoWithPlatform ,
793+ sendTo : [ "attendee@example.com" ] ,
794+ seatReferenceUid : "seat-ref-456" ,
795+ hideBranding : false ,
796+ emailSubject : "Test Subject" ,
797+ emailBody : "Cancel here: {CANCEL_URL}" ,
798+ sender : "Cal.com" ,
799+ action : WorkflowActions . EMAIL_ATTENDEE ,
800+ template : WorkflowTemplates . CUSTOM ,
801+ includeCalendarEvent : false ,
802+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
803+ } ) ;
804+
805+ expect ( result . html ) . toContain ( "seatReferenceUid=seat-ref-456" ) ;
806+ } ) ;
807+
808+ test ( "should use seatReferenceUid as uid in platform reschedule URL for seated events" , async ( ) => {
809+ const mockBookingInfoWithPlatform = {
810+ ...baseBookingInfo ,
811+ platformClientId : "platform-client-123" ,
812+ platformCancelUrl : "https://platform.example.com/cancel" ,
813+ platformRescheduleUrl : "https://platform.example.com/reschedule" ,
814+ } ;
815+
816+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
817+ evt : mockBookingInfoWithPlatform ,
818+ sendTo : [ "attendee@example.com" ] ,
819+ seatReferenceUid : "seat-ref-456" ,
820+ hideBranding : false ,
821+ emailSubject : "Test Subject" ,
822+ emailBody : "Reschedule here: {RESCHEDULE_URL}" ,
823+ sender : "Cal.com" ,
824+ action : WorkflowActions . EMAIL_ATTENDEE ,
825+ template : WorkflowTemplates . CUSTOM ,
826+ includeCalendarEvent : false ,
827+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
828+ } ) ;
829+
830+ // For seated events with EMAIL_ATTENDEE action, the reschedule URL should use seatReferenceUid
831+ expect ( result . html ) . toContain ( "https://platform.example.com/reschedule/seat-ref-456" ) ;
832+ } ) ;
833+
834+ test ( "should include allRemainingBookings=true for recurring events in platform cancel URL" , async ( ) => {
835+ const mockBookingInfoWithRecurring = {
836+ ...baseBookingInfo ,
837+ platformClientId : "platform-client-123" ,
838+ platformCancelUrl : "https://platform.example.com/cancel" ,
839+ platformRescheduleUrl : "https://platform.example.com/reschedule" ,
840+ eventType : {
841+ slug : "test-event" ,
842+ recurringEvent : { freq : 2 , count : 5 , interval : 1 } ,
843+ } ,
844+ } ;
845+
846+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
847+ evt : mockBookingInfoWithRecurring ,
848+ sendTo : [ "attendee@example.com" ] ,
849+ hideBranding : false ,
850+ emailSubject : "Test Subject" ,
851+ emailBody : "Cancel here: {CANCEL_URL}" ,
852+ sender : "Cal.com" ,
853+ action : WorkflowActions . EMAIL_ATTENDEE ,
854+ template : WorkflowTemplates . CUSTOM ,
855+ includeCalendarEvent : false ,
856+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
857+ } ) ;
858+
859+ expect ( result . html ) . toContain ( "allRemainingBookings=true" ) ;
860+ } ) ;
861+
862+ test ( "should include allRemainingBookings=false for non-recurring events in platform cancel URL" , async ( ) => {
863+ const mockBookingInfoNonRecurring = {
864+ ...baseBookingInfo ,
865+ platformClientId : "platform-client-123" ,
866+ platformCancelUrl : "https://platform.example.com/cancel" ,
867+ platformRescheduleUrl : "https://platform.example.com/reschedule" ,
868+ eventType : {
869+ slug : "test-event" ,
870+ recurringEvent : null ,
871+ } ,
872+ } ;
873+
874+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
875+ evt : mockBookingInfoNonRecurring ,
876+ sendTo : [ "attendee@example.com" ] ,
877+ hideBranding : false ,
878+ emailSubject : "Test Subject" ,
879+ emailBody : "Cancel here: {CANCEL_URL}" ,
880+ sender : "Cal.com" ,
881+ action : WorkflowActions . EMAIL_ATTENDEE ,
882+ template : WorkflowTemplates . CUSTOM ,
883+ includeCalendarEvent : false ,
884+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
885+ } ) ;
886+
887+ expect ( result . html ) . toContain ( "allRemainingBookings=false" ) ;
888+ } ) ;
889+
890+ test ( "should include allRemainingBookings=true for recurring events in standard cancel URL" , async ( ) => {
891+ const mockBookingInfoWithRecurring = {
892+ ...baseBookingInfo ,
893+ platformClientId : null ,
894+ platformCancelUrl : null ,
895+ platformRescheduleUrl : null ,
896+ eventType : {
897+ slug : "test-event" ,
898+ recurringEvent : { freq : 2 , count : 5 , interval : 1 } ,
899+ } ,
900+ } ;
901+
902+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
903+ evt : mockBookingInfoWithRecurring ,
904+ sendTo : [ "attendee@example.com" ] ,
905+ hideBranding : false ,
906+ emailSubject : "Test Subject" ,
907+ emailBody : "Cancel here: {CANCEL_URL}" ,
908+ sender : "Cal.com" ,
909+ action : WorkflowActions . EMAIL_ATTENDEE ,
910+ template : WorkflowTemplates . CUSTOM ,
911+ includeCalendarEvent : false ,
912+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
913+ } ) ;
914+
915+ // Standard cancel URL should include allRemainingBookings=true for recurring events
916+ expect ( result . html ) . toContain ( "https://cal.com/booking/booking-123" ) ;
917+ expect ( result . html ) . toContain ( "allRemainingBookings=true" ) ;
918+ } ) ;
919+
920+ test ( "should include allRemainingBookings=false for non-recurring events in standard cancel URL" , async ( ) => {
921+ const mockBookingInfoNonRecurring = {
922+ ...baseBookingInfo ,
923+ platformClientId : null ,
924+ platformCancelUrl : null ,
925+ platformRescheduleUrl : null ,
926+ eventType : {
927+ slug : "test-event" ,
928+ recurringEvent : null ,
929+ } ,
930+ } ;
931+
932+ const result = await emailWorkflowService . generateEmailPayloadForEvtWorkflow ( {
933+ evt : mockBookingInfoNonRecurring ,
934+ sendTo : [ "attendee@example.com" ] ,
935+ hideBranding : false ,
936+ emailSubject : "Test Subject" ,
937+ emailBody : "Cancel here: {CANCEL_URL}" ,
938+ sender : "Cal.com" ,
939+ action : WorkflowActions . EMAIL_ATTENDEE ,
940+ template : WorkflowTemplates . CUSTOM ,
941+ includeCalendarEvent : false ,
942+ triggerEvent : WorkflowTriggerEvents . BEFORE_EVENT ,
943+ } ) ;
944+
945+ // Standard cancel URL should include allRemainingBookings=false for non-recurring events
946+ expect ( result . html ) . toContain ( "https://cal.com/booking/booking-123" ) ;
947+ expect ( result . html ) . toContain ( "allRemainingBookings=false" ) ;
948+ } ) ;
949+ } ) ;
950+
608951 describe ( "generateEmailPayloadForEvtWorkflow - Auto Translation" , ( ) => {
609952 const mockBookingInfo = {
610953 uid : "booking-123" ,
0 commit comments