diff --git a/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts index d99630aaa6..89744b284f 100644 --- a/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/teams/workflows/controllers/org-team-workflows.controller.e2e-spec.ts @@ -27,8 +27,10 @@ import { BEFORE_EVENT, DAY, FORM_SUBMITTED, + FORM_SUBMITTED_NO_EVENT, OnAfterEventTriggerDto, OnBeforeEventTriggerDto, + OnFormSubmittedNoEventTriggerDto, OnFormSubmittedTriggerDto, } from "@/modules/workflows/inputs/workflow-trigger.input"; import { @@ -471,6 +473,64 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => { }); }); + it("should create a new routing form workflow with allowed actions and offset trigger", async () => { + const validWorkflow = structuredClone( + sampleCreateWorkflowRoutingFormDto + ) as unknown as CreateFormWorkflowDto; + validWorkflow.steps = [ + { + stepNumber: 1, + action: "email_attendee", + recipient: ATTENDEE, + template: REMINDER, + sender: "CalcomE2EStep1", + includeCalendarEvent: true, + message: { + subject: "Upcoming: {EVENT_NAME}", + html: "
Reminder for your event {EVENT_NAME}.
", + }, + }, + ]; + + validWorkflow.trigger = { + type: FORM_SUBMITTED_NO_EVENT, + offset: { + value: 1, + unit: DAY, + }, + }; + return request(app.getHttpServer()) + .post(`${basePath}/routing-form`) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .send(validWorkflow) + .expect(201) + .then((response) => { + const responseBody: GetRoutingFormWorkflowOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data.name).toEqual(sampleCreateWorkflowRoutingFormDto.name); + expect(responseBody.data.type).toEqual("routing-form"); + + if (responseBody.data.activation instanceof WorkflowFormActivationDto) { + expect(responseBody.data.activation.isActiveOnAllRoutingForms).toEqual( + sampleCreateWorkflowRoutingFormDto.activation.isActiveOnAllRoutingForms + ); + } + + expect(responseBody.data.trigger.type).toEqual(validWorkflow.trigger.type); + expect(responseBody.data.steps).toHaveLength(sampleCreateWorkflowRoutingFormDto.steps.length); + expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.id).toBeDefined(); + expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.sender).toEqual( + "CalcomE2EStep1" + ); + + const trigger = validWorkflow.trigger as OnFormSubmittedNoEventTriggerDto; + expect(responseBody.data.trigger?.type).toEqual(trigger.type); + expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit); + expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value); + }); + }); + it("should create a new workflow", async () => { return request(app.getHttpServer()) .post(basePath) diff --git a/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts b/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts index 0a40075373..7ec8ee909c 100644 --- a/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts +++ b/apps/api/v2/src/modules/workflows/inputs/create-form-workflow.ts @@ -13,7 +13,9 @@ import { import { RoutingFormWorkflowTriggerDto, FORM_SUBMITTED, + FORM_SUBMITTED_NO_EVENT, FORM_WORKFLOW_TRIGGER_TYPES, + OnFormSubmittedNoEventTriggerDto, OnFormSubmittedTriggerDto, } from "./workflow-trigger.input"; @@ -39,6 +41,7 @@ export class WorkflowFormActivationDto { @ApiExtraModels( OnFormSubmittedTriggerDto, + OnFormSubmittedNoEventTriggerDto, WorkflowEmailAddressStepDto, WorkflowEmailAttendeeStepDto, RoutingFormWorkflowTriggerDto, @@ -59,17 +62,23 @@ export class CreateFormWorkflowDto { @ApiProperty({ description: `Trigger configuration for the routing-form workflow, allowed triggers are ${FORM_WORKFLOW_TRIGGER_TYPES.toString()}`, - oneOf: [{ $ref: getSchemaPath(OnFormSubmittedTriggerDto) }], + oneOf: [ + { $ref: getSchemaPath(OnFormSubmittedTriggerDto) }, + { $ref: getSchemaPath(OnFormSubmittedNoEventTriggerDto) }, + ], }) @ValidateNested() @Type(() => RoutingFormWorkflowTriggerDto, { keepDiscriminatorProperty: true, discriminator: { property: "type", - subTypes: [{ value: OnFormSubmittedTriggerDto, name: FORM_SUBMITTED }], + subTypes: [ + { value: OnFormSubmittedTriggerDto, name: FORM_SUBMITTED }, + { value: OnFormSubmittedNoEventTriggerDto, name: FORM_SUBMITTED_NO_EVENT }, + ], }, }) - trigger!: OnFormSubmittedTriggerDto; + trigger!: OnFormSubmittedTriggerDto | OnFormSubmittedNoEventTriggerDto; @ApiProperty({ description: `Steps to execute as part of the routing-form workflow, allowed steps are ${FORM_ALLOWED_STEP_ACTIONS.toString()}`, diff --git a/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts b/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts index f6742f3c5e..d25295917e 100644 --- a/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts +++ b/apps/api/v2/src/modules/workflows/inputs/update-form-workflow.input.ts @@ -18,13 +18,16 @@ import { } from "./workflow-step.input"; import { OnFormSubmittedTriggerDto, + OnFormSubmittedNoEventTriggerDto, FORM_SUBMITTED, + FORM_SUBMITTED_NO_EVENT, FORM_WORKFLOW_TRIGGER_TYPES, RoutingFormWorkflowTriggerDto, } from "./workflow-trigger.input"; @ApiExtraModels( OnFormSubmittedTriggerDto, + OnFormSubmittedNoEventTriggerDto, UpdateEmailAddressWorkflowStepDto, UpdateEmailAttendeeWorkflowStepDto, UpdateEmailHostWorkflowStepDto, @@ -48,7 +51,10 @@ export class UpdateFormWorkflowDto { @ApiPropertyOptional({ description: `Trigger configuration for the routing-form workflow, allowed triggers are ${FORM_WORKFLOW_TRIGGER_TYPES}`, - oneOf: [{ $ref: getSchemaPath(OnFormSubmittedTriggerDto) }], + oneOf: [ + { $ref: getSchemaPath(OnFormSubmittedTriggerDto) }, + { $ref: getSchemaPath(OnFormSubmittedNoEventTriggerDto) }, + ], }) @IsOptional() @ValidateNested() @@ -56,10 +62,13 @@ export class UpdateFormWorkflowDto { keepDiscriminatorProperty: true, discriminator: { property: "type", - subTypes: [{ value: OnFormSubmittedTriggerDto, name: FORM_SUBMITTED }], + subTypes: [ + { value: OnFormSubmittedTriggerDto, name: FORM_SUBMITTED }, + { value: OnFormSubmittedNoEventTriggerDto, name: FORM_SUBMITTED_NO_EVENT }, + ], }, }) - trigger?: OnFormSubmittedTriggerDto; + trigger?: OnFormSubmittedTriggerDto | OnFormSubmittedNoEventTriggerDto; @ApiPropertyOptional({ description: `Steps to execute as part of the routing-form workflow, allowed steps are ${FORM_ALLOWED_STEP_ACTIONS.toString()}`, diff --git a/apps/api/v2/src/modules/workflows/inputs/workflow-trigger.input.ts b/apps/api/v2/src/modules/workflows/inputs/workflow-trigger.input.ts index 6789820fbb..5c1b970641 100644 --- a/apps/api/v2/src/modules/workflows/inputs/workflow-trigger.input.ts +++ b/apps/api/v2/src/modules/workflows/inputs/workflow-trigger.input.ts @@ -12,13 +12,14 @@ export const RESCHEDULE_EVENT = "rescheduleEvent"; export const AFTER_HOSTS_CAL_VIDEO_NO_SHOW = "afterHostsCalVideoNoShow"; export const AFTER_GUESTS_CAL_VIDEO_NO_SHOW = "afterGuestsCalVideoNoShow"; export const FORM_SUBMITTED = "formSubmitted"; +export const FORM_SUBMITTED_NO_EVENT = "formSubmittedNoEvent"; export const BOOKING_REJECTED = "bookingRejected"; export const BOOKING_REQUESTED = "bookingRequested"; export const BOOKING_PAYMENT_INITIATED = "bookingPaymentInitiated"; export const BOOKING_PAID = "bookingPaid"; export const BOOKING_NO_SHOW_UPDATED = "bookingNoShowUpdated"; -export const FORM_WORKFLOW_TRIGGER_TYPES = [FORM_SUBMITTED] as const; +export const FORM_WORKFLOW_TRIGGER_TYPES = [FORM_SUBMITTED, FORM_SUBMITTED_NO_EVENT] as const; export const EVENT_TYPE_WORKFLOW_TRIGGER_TYPES = [ BEFORE_EVENT, @@ -44,6 +45,7 @@ export const WORKFLOW_TRIGGER_TYPES = [ AFTER_HOSTS_CAL_VIDEO_NO_SHOW, AFTER_GUESTS_CAL_VIDEO_NO_SHOW, FORM_SUBMITTED, + FORM_SUBMITTED_NO_EVENT, BOOKING_REJECTED, BOOKING_REQUESTED, BOOKING_PAYMENT_INITIATED, @@ -60,6 +62,7 @@ export const WORKFLOW_TRIGGER_TO_ENUM = { [AFTER_HOSTS_CAL_VIDEO_NO_SHOW]: WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, [AFTER_GUESTS_CAL_VIDEO_NO_SHOW]: WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, [FORM_SUBMITTED]: WorkflowTriggerEvents.FORM_SUBMITTED, + [FORM_SUBMITTED_NO_EVENT]: WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT, [BOOKING_REJECTED]: WorkflowTriggerEvents.BOOKING_REJECTED, [BOOKING_REQUESTED]: WorkflowTriggerEvents.BOOKING_REQUESTED, [BOOKING_PAYMENT_INITIATED]: WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED, @@ -67,6 +70,19 @@ export const WORKFLOW_TRIGGER_TO_ENUM = { [BOOKING_PAID]: WorkflowTriggerEvents.BOOKING_PAID, } as const; +export const ENUM_ROUTING_FORM_WORFLOW_TRIGGERS = [ + WORKFLOW_TRIGGER_TO_ENUM[FORM_SUBMITTED_NO_EVENT], + WORKFLOW_TRIGGER_TO_ENUM[FORM_SUBMITTED], +]; + +export const ENUM_OFFSET_WORFLOW_TRIGGERS = [ + WORKFLOW_TRIGGER_TO_ENUM[FORM_SUBMITTED_NO_EVENT], + WORKFLOW_TRIGGER_TO_ENUM[BEFORE_EVENT], + WORKFLOW_TRIGGER_TO_ENUM[AFTER_EVENT], + WORKFLOW_TRIGGER_TO_ENUM[AFTER_GUESTS_CAL_VIDEO_NO_SHOW], + WORKFLOW_TRIGGER_TO_ENUM[AFTER_HOSTS_CAL_VIDEO_NO_SHOW], +]; + export const ENUM_TO_WORKFLOW_TRIGGER = { [WorkflowTriggerEvents.BEFORE_EVENT]: BEFORE_EVENT, [WorkflowTriggerEvents.EVENT_CANCELLED]: EVENT_CANCELLED, @@ -76,6 +92,7 @@ export const ENUM_TO_WORKFLOW_TRIGGER = { [WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW]: AFTER_HOSTS_CAL_VIDEO_NO_SHOW, [WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW]: AFTER_GUESTS_CAL_VIDEO_NO_SHOW, [WorkflowTriggerEvents.FORM_SUBMITTED]: FORM_SUBMITTED, + [WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT]: FORM_SUBMITTED_NO_EVENT, [WorkflowTriggerEvents.BOOKING_REJECTED]: BOOKING_REJECTED, [WorkflowTriggerEvents.BOOKING_REQUESTED]: BOOKING_REQUESTED, [WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED]: BOOKING_PAYMENT_INITIATED, @@ -205,7 +222,8 @@ export class OnNoShowUpdateTriggerDto { export class TriggerOffsetDTO { @ApiProperty({ - description: "Offset before/after the trigger time; required for BEFORE_EVENT and AFTER_EVENT only", + description: + "Offset before/after the trigger time; required for BEFORE_EVENT, AFTER_EVENT, and FORM_SUBMITTED_NO_EVENT", type: WorkflowTriggerOffsetDto, }) @ValidateNested() @@ -261,3 +279,22 @@ export class OnFormSubmittedTriggerDto { @IsIn([FORM_SUBMITTED]) type: typeof FORM_SUBMITTED = FORM_SUBMITTED; } + +export class OnFormSubmittedNoEventTriggerDto extends TriggerOffsetDTO { + @ApiProperty({ + description: "Trigger type for the workflow", + example: FORM_SUBMITTED_NO_EVENT, + }) + @IsString() + @IsIn([FORM_SUBMITTED_NO_EVENT]) + type: typeof FORM_SUBMITTED_NO_EVENT = FORM_SUBMITTED_NO_EVENT; +} + +export const OffsetTriggerDTOInstances = [ + OnFormSubmittedNoEventTriggerDto, + OnBeforeEventTriggerDto, + OnAfterEventTriggerDto, + OnAfterCalVideoGuestsNoShowTriggerDto, + OnAfterEventTriggerDto, +]; +export type OffsetTriggerDTOInstancesType = InstanceType<(typeof OffsetTriggerDTOInstances)[number]>; diff --git a/apps/api/v2/src/modules/workflows/outputs/routing-form-workflow.output.ts b/apps/api/v2/src/modules/workflows/outputs/routing-form-workflow.output.ts index d5987cf88f..d44184ae05 100644 --- a/apps/api/v2/src/modules/workflows/outputs/routing-form-workflow.output.ts +++ b/apps/api/v2/src/modules/workflows/outputs/routing-form-workflow.output.ts @@ -35,7 +35,7 @@ export class RoutingFormWorkflowTriggerOutputDto { type!: WorkflowFormTriggerType; @ApiPropertyOptional({ - description: "Offset details (present for BEFORE_EVENT/AFTER_EVENT)", + description: "Offset details (present for BEFORE_EVENT/AFTER_EVENT/FORM_SUBMITTED_NO_EVENT)", type: WorkflowTriggerOffsetOutputDto, }) @Expose() diff --git a/apps/api/v2/src/modules/workflows/services/workflows.input.service.ts b/apps/api/v2/src/modules/workflows/services/workflows.input.service.ts index d3e2154eaf..3d67bd0699 100644 --- a/apps/api/v2/src/modules/workflows/services/workflows.input.service.ts +++ b/apps/api/v2/src/modules/workflows/services/workflows.input.service.ts @@ -21,8 +21,8 @@ import { WHATSAPP_NUMBER, } from "../inputs/workflow-step.input"; import { - OnAfterEventTriggerDto, - OnBeforeEventTriggerDto, + OffsetTriggerDTOInstances, + OffsetTriggerDTOInstancesType, TIME_UNIT_TO_ENUM, WORKFLOW_TRIGGER_TO_ENUM, } from "../inputs/workflow-trigger.input"; @@ -31,6 +31,12 @@ import { export class WorkflowsInputService { constructor(private readonly teamsVerifiedResourcesRepository: TeamsVerifiedResourcesRepository) {} + private _isOffsetTrigger( + trigger: UpdateEventTypeWorkflowDto["trigger"] | UpdateFormWorkflowDto["trigger"] + ): trigger is OffsetTriggerDTOInstancesType { + return OffsetTriggerDTOInstances.some((Instance) => trigger instanceof Instance); + } + private async _getTeamPhoneNumberFromVerifiedId(teamId: number, verifiedPhoneId: number) { const phoneResource = await this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumberById( verifiedPhoneId, @@ -135,17 +141,13 @@ export class WorkflowsInputService { ? WORKFLOW_TRIGGER_TO_ENUM[updateDto?.trigger?.type] : currentData.trigger; - const timeUnitForZod = - updateDto.trigger instanceof OnBeforeEventTriggerDto || - updateDto.trigger instanceof OnAfterEventTriggerDto - ? updateDto?.trigger?.offset?.unit ?? currentData.timeUnit ?? null - : undefined; + const timeUnitForZod = this._isOffsetTrigger(updateDto.trigger) + ? updateDto?.trigger?.offset?.unit ?? currentData.timeUnit ?? null + : undefined; - const time = - updateDto.trigger instanceof OnBeforeEventTriggerDto || - updateDto.trigger instanceof OnAfterEventTriggerDto - ? updateDto?.trigger?.offset?.value ?? currentData?.time ?? null - : null; + const time = this._isOffsetTrigger(updateDto.trigger) + ? updateDto?.trigger?.offset?.value ?? currentData?.time ?? null + : null; const timeUnit = timeUnitForZod ? TIME_UNIT_TO_ENUM[timeUnitForZod] : null; diff --git a/apps/api/v2/src/modules/workflows/services/workflows.output.service.ts b/apps/api/v2/src/modules/workflows/services/workflows.output.service.ts index 3bf96d06aa..f810662a8b 100644 --- a/apps/api/v2/src/modules/workflows/services/workflows.output.service.ts +++ b/apps/api/v2/src/modules/workflows/services/workflows.output.service.ts @@ -32,13 +32,8 @@ import { WHATSAPP_NUMBER, } from "../inputs/workflow-step.input"; import { - AFTER_EVENT, - AFTER_GUESTS_CAL_VIDEO_NO_SHOW, - AFTER_HOSTS_CAL_VIDEO_NO_SHOW, - BEFORE_EVENT, ENUM_TO_TIME_UNIT, ENUM_TO_WORKFLOW_TRIGGER, - FORM_SUBMITTED, HOUR, OnAfterCalVideoGuestsNoShowTriggerDto, OnAfterCalVideoHostsNoShowTriggerDto, @@ -47,6 +42,7 @@ import { OnCancelTriggerDto, OnCreationTriggerDto, OnFormSubmittedTriggerDto, + OnFormSubmittedNoEventTriggerDto, OnNoShowUpdateTriggerDto, OnPaidTriggerDto, OnPaymentInitiatedTriggerDto, @@ -54,6 +50,9 @@ import { OnRequestedTriggerDto, OnRescheduleTriggerDto, WORKFLOW_TRIGGER_TO_ENUM, + FORM_WORKFLOW_TRIGGER_TYPES, + ENUM_ROUTING_FORM_WORFLOW_TRIGGERS, + ENUM_OFFSET_WORFLOW_TRIGGERS, } from "../inputs/workflow-trigger.input"; export type TriggerDtoType = @@ -64,6 +63,7 @@ export type TriggerDtoType = | OnCancelTriggerDto | OnAfterCalVideoGuestsNoShowTriggerDto | OnFormSubmittedTriggerDto + | OnFormSubmittedNoEventTriggerDto | OnRejectedTriggerDto | OnRequestedTriggerDto | OnPaymentInitiatedTriggerDto @@ -141,10 +141,20 @@ const ACTION_CONFIG_MAP = { @Injectable() export class WorkflowsOutputService { _isFormAllowedStepAction(action: StepAction): action is FormAllowedStepAction { - if (FORM_ALLOWED_STEP_ACTIONS.some((formAction) => formAction === action)) { - return true; - } - return false; + return FORM_ALLOWED_STEP_ACTIONS.some((formAction) => formAction === action); + } + _isFormAllowedTrigger( + trigger: WorkflowType["trigger"] + ): trigger is (typeof ENUM_ROUTING_FORM_WORFLOW_TRIGGERS)[number] { + return FORM_WORKFLOW_TRIGGER_TYPES.some( + (formTrigger) => WORKFLOW_TRIGGER_TO_ENUM[formTrigger] === trigger + ); + } + + private _isOffsetTrigger( + trigger: WorkflowType["trigger"] + ): trigger is (typeof ENUM_OFFSET_WORFLOW_TRIGGERS)[number] { + return ENUM_OFFSET_WORFLOW_TRIGGERS.some((offsetTrigger) => offsetTrigger === trigger); } /** @@ -200,14 +210,22 @@ export class WorkflowsOutputService { } toRoutingFormOutputDto(workflow: WorkflowType): RoutingFormWorkflowOutput | void { - if (workflow.type === "ROUTING_FORM" && workflow.trigger === WORKFLOW_TRIGGER_TO_ENUM[FORM_SUBMITTED]) { + if (workflow.type === "ROUTING_FORM" && this._isFormAllowedTrigger(workflow.trigger)) { const activation: WorkflowFormActivationDto = { isActiveOnAllRoutingForms: workflow.isActiveOnAll, activeOnRoutingFormIds: workflow.activeOnRoutingForms?.map((relation) => relation.routingFormId) ?? [], }; - const trigger: TriggerDtoType = { type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger] }; + const trigger: TriggerDtoType = this._isOffsetTrigger(workflow.trigger) + ? { + type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger], + offset: { + value: workflow.time ?? 1, + unit: workflow.timeUnit ? ENUM_TO_TIME_UNIT[workflow.timeUnit] : HOUR, + }, + } + : { type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger] }; const steps: RoutingFormWorkflowStepOutputDto[] = workflow.steps.map((step) => { return this.mapStep(step, "routing-form"); @@ -225,25 +243,21 @@ export class WorkflowsOutputService { } toEventTypeOutputDto(workflow: WorkflowType): EventTypeWorkflowOutput | void { - if (workflow.type === "EVENT_TYPE" && workflow.trigger !== WORKFLOW_TRIGGER_TO_ENUM[FORM_SUBMITTED]) { + if (workflow.type === "EVENT_TYPE" && !this._isFormAllowedTrigger(workflow.trigger)) { const activation: WorkflowActivationDto = { isActiveOnAllEventTypes: workflow.isActiveOnAll, activeOnEventTypeIds: workflow.activeOn?.map((relation) => relation.eventTypeId) ?? [], }; - const trigger: TriggerEventTypeDtoType = - workflow.trigger === WORKFLOW_TRIGGER_TO_ENUM[BEFORE_EVENT] || - workflow.trigger === WORKFLOW_TRIGGER_TO_ENUM[AFTER_EVENT] || - workflow.trigger === WORKFLOW_TRIGGER_TO_ENUM[AFTER_GUESTS_CAL_VIDEO_NO_SHOW] || - workflow.trigger === WORKFLOW_TRIGGER_TO_ENUM[AFTER_HOSTS_CAL_VIDEO_NO_SHOW] - ? { - type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger], - offset: { - value: workflow.time ?? 1, - unit: workflow.timeUnit ? ENUM_TO_TIME_UNIT[workflow.timeUnit] : HOUR, - }, - } - : { type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger] }; + const trigger: TriggerEventTypeDtoType = this._isOffsetTrigger(workflow.trigger) + ? { + type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger], + offset: { + value: workflow.time ?? 1, + unit: workflow.timeUnit ? ENUM_TO_TIME_UNIT[workflow.timeUnit] : HOUR, + }, + } + : { type: ENUM_TO_WORKFLOW_TRIGGER[workflow.trigger] }; const steps: EventTypeWorkflowStepOutputDto[] = workflow.steps.map((step) => { return this.mapStep(step, "event-type"); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index ce2c3c4f43..4f1e6f4508 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3717,6 +3717,8 @@ "stats": "Stats", "booking_status": "Booking status", "form_submitted_trigger": "When routing form is submitted", + "form_submitted_no_event_trigger": "When routing form is submitted and no booking is created", + "how_long_after_form_submitted_no_event": "How long after the form was submitted?", "visit": "Visit", "location_custom_label_input_label": "Custom label on booking page", "meeting_link": "Meeting link", diff --git a/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts b/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts index 8ff6469e32..03adbcd2a8 100644 --- a/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts +++ b/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts @@ -141,6 +141,59 @@ describe("_onFormSubmission", () => { }, name: { value: "Test Name", response: "Test Name" }, }, + responseId, + form: { + ...mockForm, + fields: mockForm.fields.map((field) => ({ + type: field.type, + identifier: field.identifier, + })), + }, + }); + }); + + it("should call WorkflowService.scheduleFormWorkflows for FORM_SUBMITTED_NO_EVENT workflows", async () => { + const mockWorkflows = [ + { + id: 2, + name: "Form Follow-up Workflow", + userId: 1, + teamId: null, + trigger: WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT, + time: 30, + timeUnit: "MINUTE", + steps: [ + { + id: 2, + action: WorkflowActions.EMAIL_ATTENDEE, + sendTo: null, + reminderBody: "Follow up on your form submission", + emailSubject: "Follow Up", + template: WorkflowTemplates.CUSTOM, + verifiedAt: new Date(), + includeCalendarEvent: false, + numberVerificationPending: false, + numberRequired: false, + }, + ], + }, + ]; + + vi.mocked(WorkflowService.getAllWorkflowsFromRoutingForm).mockResolvedValueOnce(mockWorkflows as any); + + await _onFormSubmission(mockForm as any, mockResponse, responseId); + + expect(WorkflowService.getAllWorkflowsFromRoutingForm).toHaveBeenCalledWith(mockForm); + expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith({ + workflows: mockWorkflows, + responses: { + email: { + value: "test@response.com", + response: "test@response.com", + }, + name: { value: "Test Name", response: "Test Name" }, + }, + responseId, form: { ...mockForm, fields: mockForm.fields.map((field) => ({ diff --git a/packages/app-store/routing-forms/lib/formSubmissionUtils.ts b/packages/app-store/routing-forms/lib/formSubmissionUtils.ts index 4147942d0f..8c24ece10c 100644 --- a/packages/app-store/routing-forms/lib/formSubmissionUtils.ts +++ b/packages/app-store/routing-forms/lib/formSubmissionUtils.ts @@ -222,6 +222,7 @@ export async function _onFormSubmission( await WorkflowService.scheduleFormWorkflows({ workflows, + responseId, responses: fieldResponsesByIdentifier, form: { ...form, diff --git a/packages/features/bookings/repositories/BookingRepository.ts b/packages/features/bookings/repositories/BookingRepository.ts index 4aaf5c641b..d7c1643511 100644 --- a/packages/features/bookings/repositories/BookingRepository.ts +++ b/packages/features/bookings/repositories/BookingRepository.ts @@ -386,6 +386,21 @@ export class BookingRepository { }); } + async findFirstBookingFromResponse({ responseId }: { responseId: number }) { + const booking = await this.prismaClient.booking.findFirst({ + where: { + routedFromRoutingFormReponse: { + id: responseId, + }, + }, + select: { + id: true, + }, + }); + + return booking; + } + async findBookingByUidWithEventType({ bookingUid }: { bookingUid: string }) { return await this.prismaClient.booking.findUnique({ where: { diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index ee47cc644b..04620593c9 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -114,6 +114,7 @@ const getTimeSectionText = (trigger: WorkflowTriggerEvents, t: TFunction) => { [WorkflowTriggerEvents.BEFORE_EVENT]: "how_long_before", [WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW]: "how_long_after_hosts_no_show", [WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW]: "how_long_after_guests_no_show", + [WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT]: "how_long_after_form_submitted_no_event", }; return triggerMap[trigger] ? t(triggerMap[trigger]) : null; }; diff --git a/packages/features/ee/workflows/lib/constants.ts b/packages/features/ee/workflows/lib/constants.ts index 4ebff5b077..a9703887b0 100644 --- a/packages/features/ee/workflows/lib/constants.ts +++ b/packages/features/ee/workflows/lib/constants.ts @@ -9,6 +9,7 @@ export const WORKFLOW_TRIGGER_EVENTS = [ WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, WorkflowTriggerEvents.FORM_SUBMITTED, + WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT, WorkflowTriggerEvents.BOOKING_REJECTED, WorkflowTriggerEvents.BOOKING_REQUESTED, WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED, @@ -96,9 +97,13 @@ export const IMMEDIATE_WORKFLOW_TRIGGER_EVENTS: WorkflowTriggerEvents[] = [ WorkflowTriggerEvents.BOOKING_REJECTED, WorkflowTriggerEvents.BOOKING_REQUESTED, WorkflowTriggerEvents.FORM_SUBMITTED, + WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT, // no real immediate workflow but it's scheduled with tasker ]; -export const FORM_TRIGGER_WORKFLOW_EVENTS: WorkflowTriggerEvents[] = [WorkflowTriggerEvents.FORM_SUBMITTED]; +export const FORM_TRIGGER_WORKFLOW_EVENTS: WorkflowTriggerEvents[] = [ + WorkflowTriggerEvents.FORM_SUBMITTED, + WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT, +]; export const ALLOWED_FORM_WORKFLOW_ACTIONS = [ WorkflowActions.EMAIL_ATTENDEE, diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index 0762cb64eb..d1f2609127 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -9,7 +9,7 @@ import { import { sendOrScheduleWorkflowEmails } from "@calcom/features/ee/workflows/lib/reminders/providers/emailProvider"; import * as twilio from "@calcom/features/ee/workflows/lib/reminders/providers/twilioProvider"; import type { Workflow, WorkflowStep } from "@calcom/features/ee/workflows/lib/types"; -import { getSubmitterEmail } from "@calcom/features/tasker/tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWebhook"; +import { getSubmitterEmail } from "@calcom/features/tasker/tasks/triggerFormSubmittedNoEvent/formSubmissionValidation"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError"; import { SENDER_NAME } from "@calcom/lib/constants"; diff --git a/packages/features/tasker/tasker.ts b/packages/features/tasker/tasker.ts index 8d145eeacb..4808c17cfd 100644 --- a/packages/features/tasker/tasker.ts +++ b/packages/features/tasker/tasker.ts @@ -14,6 +14,9 @@ type TaskPayloads = { triggerFormSubmittedNoEventWebhook: z.infer< typeof import("./tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWebhook").ZTriggerFormSubmittedNoEventWebhookPayloadSchema >; + triggerFormSubmittedNoEventWorkflow: z.infer< + typeof import("./tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWorkflow").ZTriggerFormSubmittedNoEventWorkflowPayloadSchema + >; translateEventTypeData: z.infer< typeof import("./tasks/translateEventTypeData").ZTranslateEventDataPayloadSchema >; diff --git a/packages/features/tasker/tasks/index.ts b/packages/features/tasker/tasks/index.ts index 2bff98c396..640caa3dee 100644 --- a/packages/features/tasker/tasks/index.ts +++ b/packages/features/tasker/tasks/index.ts @@ -18,6 +18,10 @@ const tasks: Record