Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
91f2c7e
fix: break circular dependency in messageDispatcher via dependency in…
devin-ai-integration[bot] Nov 22, 2025
f120f70
feat: wire creditCheckFn from all callers to complete circular depend…
devin-ai-integration[bot] Nov 24, 2025
ec34c0a
fix: use generic type with type guard in logFailedResults to fix type…
devin-ai-integration[bot] Nov 24, 2025
ba7ae4c
wip
hbjORbj Nov 24, 2025
3db6d92
wip
hbjORbj Nov 24, 2025
7c96fb8
wip
hbjORbj Nov 24, 2025
4f4cf37
revert
hbjORbj Nov 25, 2025
2821381
revert
hbjORbj Nov 25, 2025
c1defd0
feat: wire creditCheckFn from all remaining callers to eliminate fall…
devin-ai-integration[bot] Nov 25, 2025
05eecdd
test: update formSubmissionUtils tests to expect creditCheckFn parameter
devin-ai-integration[bot] Nov 25, 2025
cbe82b4
test: update sms-manager test to expect creditCheckFn parameter
devin-ai-integration[bot] Nov 25, 2025
0c693ee
feat: make creditCheckFn required to fully break circular dependency
devin-ai-integration[bot] Nov 25, 2025
a17ef40
fix: make creditCheckFn required in WorkflowService.scheduleFormWorkf…
devin-ai-integration[bot] Nov 25, 2025
ff1c748
remove
hbjORbj Nov 25, 2025
f550ec5
fix
hbjORbj Nov 25, 2025
a4b4255
Merge remote-tracking branch 'origin/main' into devin/messagedispatch…
hbjORbj Nov 25, 2025
d72f730
refactor
hbjORbj Nov 25, 2025
dfc0b7a
refactor
hbjORbj Nov 25, 2025
ef5ee6b
refactor
hbjORbj Nov 25, 2025
6c0fc96
wip
hbjORbj Nov 25, 2025
d1e45cb
fix
hbjORbj Nov 25, 2025
e9227a5
fix
hbjORbj Nov 25, 2025
e45282c
Merge branch 'main' into devin/messagedispatcher-circular-1763790475
hbjORbj Nov 26, 2025
e379c69
rm
hbjORbj Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 63 additions & 54 deletions packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,25 +158,28 @@ describe("_onFormSubmission", () => {
await _onFormSubmission(mockForm, mockResponse, responseId);

expect(WorkflowService.getAllWorkflowsFromRoutingForm).toHaveBeenCalledWith(mockForm);
expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith({
workflows: mockWorkflows,
responses: {
email: {
value: "[email protected]",
response: "[email protected]",
expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith(
expect.objectContaining({
workflows: mockWorkflows,
responses: {
email: {
value: "[email protected]",
response: "[email protected]",
},
name: { value: "Test Name", response: "Test Name" },
},
name: { value: "Test Name", response: "Test Name" },
},
responseId,
routedEventTypeId: null,
form: {
...mockForm,
fields: mockForm.fields.map((field) => ({
type: field.type,
identifier: field.identifier,
})),
},
});
responseId,
routedEventTypeId: null,
form: {
...mockForm,
fields: mockForm.fields.map((field) => ({
type: field.type,
identifier: field.identifier,
})),
},
creditCheckFn: expect.any(Function),
})
);
});

it("should call WorkflowService.scheduleFormWorkflows for FORM_SUBMITTED_NO_EVENT workflows", async () => {
Expand Down Expand Up @@ -212,25 +215,28 @@ describe("_onFormSubmission", () => {
await _onFormSubmission(mockForm, mockResponse, responseId);

expect(WorkflowService.getAllWorkflowsFromRoutingForm).toHaveBeenCalledWith(mockForm);
expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith({
workflows: mockWorkflows,
responses: {
email: {
value: "[email protected]",
response: "[email protected]",
expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith(
expect.objectContaining({
workflows: mockWorkflows,
responses: {
email: {
value: "[email protected]",
response: "[email protected]",
},
name: { value: "Test Name", response: "Test Name" },
},
name: { value: "Test Name", response: "Test Name" },
},
routedEventTypeId: null,
responseId,
form: {
...mockForm,
fields: mockForm.fields.map((field) => ({
type: field.type,
identifier: field.identifier,
})),
},
});
routedEventTypeId: null,
responseId,
form: {
...mockForm,
fields: mockForm.fields.map((field) => ({
type: field.type,
identifier: field.identifier,
})),
},
creditCheckFn: expect.any(Function),
})
);
});

it("should pass routedEventTypeId when chosenAction is eventTypeRedirectUrl", async () => {
Expand Down Expand Up @@ -271,25 +277,28 @@ describe("_onFormSubmission", () => {

await _onFormSubmission(mockForm, mockResponse, responseId, chosenAction);

expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith({
workflows: mockWorkflows,
responses: {
email: {
value: "[email protected]",
response: "[email protected]",
expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith(
expect.objectContaining({
workflows: mockWorkflows,
responses: {
email: {
value: "[email protected]",
response: "[email protected]",
},
name: { value: "Test Name", response: "Test Name" },
},
name: { value: "Test Name", response: "Test Name" },
},
routedEventTypeId: 42,
responseId,
form: {
...mockForm,
fields: mockForm.fields.map((field) => ({
type: field.type,
identifier: field.identifier,
})),
},
});
routedEventTypeId: 42,
responseId,
form: {
...mockForm,
fields: mockForm.fields.map((field) => ({
type: field.type,
identifier: field.identifier,
})),
},
creditCheckFn: expect.any(Function),
})
);
});
});

Expand Down
5 changes: 5 additions & 0 deletions packages/app-store/routing-forms/lib/formSubmissionUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import dayjs from "@calcom/dayjs";
import { CreditService } from "@calcom/features/ee/billing/credit-service";
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
import type { Tasker } from "@calcom/features/tasker/tasker";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
Expand Down Expand Up @@ -224,11 +225,15 @@ export async function _onFormSubmission(
chosenAction && chosenAction.type === "eventTypeRedirectUrl" && chosenAction.eventTypeId
? chosenAction.eventTypeId
: null;

const creditService = new CreditService();

await WorkflowService.scheduleFormWorkflows({
workflows,
responseId,
responses: fieldResponsesByIdentifier,
routedEventTypeId,
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
form: {
...form,
fields: form.fields.map((field) => ({
Expand Down
4 changes: 4 additions & 0 deletions packages/features/bookings/lib/handleBookingRequested.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { sendAttendeeRequestEmailAndSMS, sendOrganizerRequestEmail } from "@calcom/emails/email-manager";
import { getWebhookPayloadForBooking } from "@calcom/features/bookings/lib/getWebhookPayloadForBooking";
import { CreditService } from "@calcom/features/ee/billing/credit-service";
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
Expand Down Expand Up @@ -103,6 +104,8 @@ export async function handleBookingRequested(args: {

const workflows = await getAllWorkflowsFromEventType(booking.eventType, booking.userId);
if (workflows.length > 0) {
const creditService = new CreditService();

await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({
workflows,
smsReminderNumber: booking.smsReminderNumber,
Expand All @@ -117,6 +120,7 @@ export async function handleBookingRequested(args: {
},
},
triggers: [WorkflowTriggerEvents.BOOKING_REQUESTED],
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
});
}
} catch (error) {
Expand Down
4 changes: 4 additions & 0 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import EventManager from "@calcom/features/bookings/lib/EventManager";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { processNoShowFeeOnCancellation } from "@calcom/features/bookings/lib/payment/processNoShowFeeOnCancellation";
import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund";
import { CreditService } from "@calcom/features/ee/billing/credit-service";
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
Expand Down Expand Up @@ -353,6 +354,8 @@ async function handler(input: CancelBookingInput) {
const workflows = await getAllWorkflowsFromEventType(bookingToDelete.eventType, bookingToDelete.userId);
const parsedMetadata = bookingMetadataSchema.safeParse(bookingToDelete.metadata || {});

const creditService = new CreditService();

await sendCancelledReminders({
workflows,
smsReminderNumber: bookingToDelete.smsReminderNumber,
Expand All @@ -371,6 +374,7 @@ async function handler(input: CancelBookingInput) {
},
},
hideBranding: !!bookingToDelete.eventType?.owner?.hideBranding,
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
});

let updatedBookings: {
Expand Down
7 changes: 7 additions & 0 deletions packages/features/bookings/lib/handleConfirmation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/sc
import { sendScheduledEmailsAndSMS } from "@calcom/emails/email-manager";
import type { EventManagerUser } from "@calcom/features/bookings/lib/EventManager";
import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager";
import { CreditService } from "@calcom/features/ee/billing/credit-service";
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
import {
allowDisablingAttendeeConfirmationEmails,
Expand Down Expand Up @@ -360,6 +361,8 @@ export async function handleConfirmation(args: {
});
}

const creditService = new CreditService();

await WorkflowService.scheduleWorkflowsForNewBooking({
workflows,
smsReminderNumber: updatedBookings[index].smsReminderNumber,
Expand All @@ -368,6 +371,7 @@ export async function handleConfirmation(args: {
isConfirmedByDefault: true,
isNormalBookingOrFirstRecurringSlot: isFirstBooking,
isRescheduleEvent: false,
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
});
}
} catch (error) {
Expand Down Expand Up @@ -571,12 +575,15 @@ export async function handleConfirmation(args: {
metadata: { videoCallUrl: meetingUrl },
};

const creditService = new CreditService();

await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({
workflows,
smsReminderNumber: booking.smsReminderNumber,
calendarEvent: calendarEventForWorkflow,
hideBranding: !!updatedBookings[0].eventType?.owner?.hideBranding,
triggers: [WorkflowTriggerEvents.BOOKING_PAID],
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
});
} catch (error) {
log.error("Error while scheduling workflow reminders for booking paid", safeStringify(error));
Expand Down
5 changes: 4 additions & 1 deletion packages/features/bookings/lib/handleSeats/handleSeats.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import dayjs from "@calcom/dayjs";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import { CreditService } from "@calcom/features/ee/billing/credit-service";
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
import type { EventPayloadType } from "@calcom/features/webhooks/lib/sendPayload";
import { ErrorCode } from "@calcom/lib/errorCodes";
Expand Down Expand Up @@ -108,6 +108,8 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => {
...reqBodyMetadata,
};
try {
const creditService = new CreditService();

await WorkflowService.scheduleWorkflowsForNewBooking({
workflows: workflows,
smsReminderNumber: smsReminderNumber || null,
Expand All @@ -130,6 +132,7 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => {
isConfirmedByDefault: !evt.requiresConfirmation,
isRescheduleEvent: !!rescheduleUid,
isNormalBookingOrFirstRecurringSlot: true,
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
});
} catch (error) {
loggerWithEventDetails.error("Error while scheduling workflow reminders", JSON.stringify({ error }));
Expand Down
45 changes: 26 additions & 19 deletions packages/features/bookings/lib/service/RegularBookingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhoo
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import { BookingEventHandlerService } from "@calcom/features/bookings/lib/onBookingEvents/BookingEventHandlerService";
import { getSpamCheckService } from "@calcom/features/di/watchlist/containers/SpamCheckService.container";
import { CreditService } from "@calcom/features/ee/billing/credit-service";
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
import AssignmentReasonRecorder from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
Expand Down Expand Up @@ -1261,9 +1262,9 @@ async function handler(
// This ensures that createMeeting isn't called for static video apps as bookingLocation becomes just a regular value for them.
const { bookingLocation, conferenceCredentialId } = organizerOrFirstDynamicGroupMemberDefaultLocationUrl
? {
bookingLocation: organizerOrFirstDynamicGroupMemberDefaultLocationUrl,
conferenceCredentialId: undefined,
}
bookingLocation: organizerOrFirstDynamicGroupMemberDefaultLocationUrl,
conferenceCredentialId: undefined,
}
: getLocationValueForDB(locationBodyString, eventType.locations);

tracingLogger.info("locationBodyString", locationBodyString);
Expand Down Expand Up @@ -1309,8 +1310,8 @@ async function handler(
const destinationCalendar = eventType.destinationCalendar
? [eventType.destinationCalendar]
: organizerUser.destinationCalendar
? [organizerUser.destinationCalendar]
: null;
? [organizerUser.destinationCalendar]
: null;

let organizerEmail = organizerUser.email || "Email-less";
if (eventType.useEventTypeDestinationCalendarEmail && destinationCalendar?.[0]?.primaryEmail) {
Expand Down Expand Up @@ -1931,14 +1932,14 @@ async function handler(
}
const updateManager = !skipCalendarSyncTaskCreation
? await eventManager.reschedule(
evt,
originalRescheduledBooking.uid,
undefined,
changedOrganizer,
previousHostDestinationCalendar,
isBookingRequestedReschedule,
skipDeleteEventsAndMeetings
)
evt,
originalRescheduledBooking.uid,
undefined,
changedOrganizer,
previousHostDestinationCalendar,
isBookingRequestedReschedule,
skipDeleteEventsAndMeetings
)
: placeholderCreatedEvent;
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
Expand Down Expand Up @@ -2237,8 +2238,8 @@ async function handler(

const metadata = videoCallUrl
? {
videoCallUrl: getVideoCallUrlFromCalEvent(evt) || videoCallUrl,
}
videoCallUrl: getVideoCallUrlFromCalEvent(evt) || videoCallUrl,
}
: undefined;

const bookingFlowConfig = {
Expand Down Expand Up @@ -2331,9 +2332,9 @@ async function handler(
...eventType,
metadata: eventType.metadata
? {
...eventType.metadata,
apps: eventType.metadata?.apps as Prisma.JsonValue,
}
...eventType.metadata,
apps: eventType.metadata?.apps as Prisma.JsonValue,
}
: {},
},
paymentAppCredentials: eventTypePaymentAppCredential as IEventTypePaymentCredentialType,
Expand Down Expand Up @@ -2377,6 +2378,8 @@ async function handler(
};

if (isNormalBookingOrFirstRecurringSlot) {
const creditService = new CreditService();

await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({
workflows,
smsReminderNumber: smsReminderNumber || null,
Expand All @@ -2385,6 +2388,7 @@ async function handler(
seatReferenceUid: evt.attendeeSeatId,
isDryRun,
triggers: [WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED],
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
});
}
} catch (error) {
Expand Down Expand Up @@ -2553,6 +2557,8 @@ async function handler(
}

try {
const creditService = new CreditService();

await WorkflowService.scheduleWorkflowsForNewBooking({
workflows,
smsReminderNumber: smsReminderNumber || null,
Expand All @@ -2563,6 +2569,7 @@ async function handler(
isConfirmedByDefault,
isNormalBookingOrFirstRecurringSlot,
isRescheduleEvent: !!rescheduleUid,
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
});
} catch (error) {
tracingLogger.error("Error while scheduling workflow reminders", JSON.stringify({ error }));
Expand Down Expand Up @@ -2630,7 +2637,7 @@ async function handler(
* We are open to renaming it to something more descriptive.
*/
export class RegularBookingService implements IBookingService {
constructor(private readonly deps: IBookingServiceDependencies) { }
constructor(private readonly deps: IBookingServiceDependencies) {}

async createBooking(input: { bookingData: CreateRegularBookingData; bookingMeta?: CreateBookingMeta }) {
return handler({ bookingData: input.bookingData, ...input.bookingMeta }, this.deps);
Expand Down
Loading