From dbc6b15ffc6645d069e259b0fe588b56786568be Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:26:55 +0200 Subject: [PATCH] fix: time and timeUnit partial update workflows api v2 (#26503) * test: add e2e test for workflow partial update time/timeUnit preservation Co-Authored-By: morgan@cal.com * fix: time and timeUnit partial update workflows api v2 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org-team-workflows.controller.e2e-spec.ts | 54 +++++++++++++++++++ .../services/workflows.input.service.ts | 46 ++++++++++------ 2 files changed, 84 insertions(+), 16 deletions(-) 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 54cfe2b78b..feb56635b8 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 @@ -905,6 +905,60 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => { .send(partialUpdateDto) .expect(401); }); + + it("should preserve time and timeUnit when not provided in partial update", async () => { + const workflowWithOffset = await request(app.getHttpServer()) + .post(basePath) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .send({ + name: `Workflow With Offset ${randomString()}`, + activation: { + isActiveOnAllEventTypes: true, + activeOnEventTypeIds: [], + }, + trigger: { + type: BEFORE_EVENT, + offset: { + value: 2, + unit: DAY, + }, + }, + steps: [ + { + stepNumber: 1, + action: "email_attendee", + recipient: ATTENDEE, + template: REMINDER, + sender: "CalcomE2ETest", + includeCalendarEvent: true, + message: { + subject: "Upcoming: {EVENT_NAME}", + html: "

Reminder for your event {EVENT_NAME}.

", + }, + }, + ], + }) + .expect(201); + + const workflowId = workflowWithOffset.body.data.id; + expect(workflowWithOffset.body.data.trigger?.offset?.value).toEqual(2); + expect(workflowWithOffset.body.data.trigger?.offset?.unit).toEqual(DAY); + + const partialUpdateDto = { + name: `Updated Workflow Name ${randomString()}`, + }; + + const updatedWorkflow = await request(app.getHttpServer()) + .patch(`${basePath}/${workflowId}`) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .send(partialUpdateDto) + .expect(200); + + expect(updatedWorkflow.body.data.trigger?.offset?.value).toEqual(2); + expect(updatedWorkflow.body.data.trigger?.offset?.unit).toEqual(DAY); + + await workflowsRepositoryFixture.delete(workflowId); + }); }); describe(`DELETE ${basePath}/:workflowId`, () => { 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 3d67bd0699..bb1486a0eb 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 @@ -129,27 +129,41 @@ export class WorkflowsInputService { teamId: number, workflowIdToUse: number ) { - const mappedSteps = updateDto?.steps - ? await Promise.all( - updateDto.steps.map(async (stepDto: UpdateWorkflowStepDto, index: number) => - this.mapUpdateWorkflowStepToZodUpdateSchema(stepDto, index, teamId, workflowIdToUse) - ) + // 1. Map Steps + let mappedSteps; + if (updateDto?.steps) { + mappedSteps = await Promise.all( + updateDto.steps.map(async (stepDto: UpdateWorkflowStepDto, index: number) => + this.mapUpdateWorkflowStepToZodUpdateSchema(stepDto, index, teamId, workflowIdToUse) ) - : currentData.steps.map((step) => ({ ...step, senderName: step.sender })); + ); + } else { + mappedSteps = currentData.steps.map((step) => ({ + ...step, + senderName: step.sender, + })); + } - const triggerForZod = updateDto?.trigger?.type - ? WORKFLOW_TRIGGER_TO_ENUM[updateDto?.trigger?.type] - : currentData.trigger; + // 2. Map Trigger + let triggerForZod = currentData.trigger; + if (updateDto?.trigger?.type) { + triggerForZod = WORKFLOW_TRIGGER_TO_ENUM[updateDto.trigger.type]; + } - const timeUnitForZod = this._isOffsetTrigger(updateDto.trigger) - ? updateDto?.trigger?.offset?.unit ?? currentData.timeUnit ?? null - : undefined; + // 3. Map Time and TimeUnit (Keeping currentData if trigger is missing or not an offset) + let timeUnitForZod = (currentData.timeUnit?.toLowerCase() ?? null) as "hour" | "minute" | "day" | null; + let time = currentData.time ?? null; - const time = this._isOffsetTrigger(updateDto.trigger) - ? updateDto?.trigger?.offset?.value ?? currentData?.time ?? null - : null; + if (updateDto.trigger && this._isOffsetTrigger(updateDto.trigger)) { + timeUnitForZod = updateDto.trigger.offset?.unit ?? timeUnitForZod ?? null; + time = updateDto.trigger.offset?.value ?? currentData.time ?? null; + } - const timeUnit = timeUnitForZod ? TIME_UNIT_TO_ENUM[timeUnitForZod] : null; + // 4. Final Enum Conversion + let timeUnit = null; + if (timeUnitForZod) { + timeUnit = TIME_UNIT_TO_ENUM[timeUnitForZod]; + } return { mappedSteps, triggerForZod, time, timeUnit }; }