-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/calendar sync #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
78ba17e
6b6d00d
79de50b
11d1bd2
fc5b3f9
4daf6d6
f027bfc
118a88a
47a231d
da76f39
c6b45ea
55d484b
36eadd9
1d2e0f1
c5f8fab
739e3bc
d9891c1
e325552
72f30ae
088aa3f
10b8607
14a8c6a
b6dc0bf
30b2ad2
a683faa
b486347
075e055
b1ef7b6
0b8d5d6
7c267ad
5e8ed4f
2605eee
68bde1e
c556e66
6066bf0
d34b878
f147197
a967b42
95bf75f
fdbf170
43e892d
35b4f6b
5238b0d
153a85e
f707d1e
a251399
57c1133
87d4e0d
cf10b33
81ffd24
9d514ab
0b887e1
893d1e5
1e430fe
3ef231f
3928bbc
032dbed
68e26b1
23fbfba
241c46a
fcbddb3
0d25b29
4458e35
b2f2a03
1bddf12
34c7abb
8ef1e6c
2dc1985
327e9f4
9bb49b7
d8a72b8
946e9ea
6cb5e3b
b75e507
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -162,6 +162,7 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) { | |
| cancelSubsequentBookings, | ||
| internalNote, | ||
| skipCancellationReasonValidation = false, | ||
| skipCalendarSyncTaskCancellation = false, | ||
| } = bookingCancelInput.parse(body); | ||
| const bookingToDelete = await getBookingToDelete(id, uid); | ||
| const { | ||
|
|
@@ -610,36 +611,46 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) { | |
| allRemainingBookings | ||
| ); | ||
|
|
||
| try { | ||
| const bookingToDeleteEventTypeMetadataParsed = eventTypeMetaDataSchemaWithTypedApps.safeParse( | ||
| bookingToDelete.eventType?.metadata || null | ||
| ); | ||
|
|
||
| if (!bookingToDeleteEventTypeMetadataParsed.success) { | ||
| log.error( | ||
| `Error parsing metadata`, | ||
| safeStringify({ error: bookingToDeleteEventTypeMetadataParsed?.error }) | ||
| // Skip calendar event deletion when cancellation comes from a calendar subscription webhook | ||
| // to avoid infinite loops (Google/Office365 → Cal.com → Google/Office365 → ...) | ||
| if (!skipCalendarSyncTaskCancellation) { | ||
| try { | ||
| const bookingToDeleteEventTypeMetadataParsed = eventTypeMetaDataSchemaWithTypedApps.safeParse( | ||
| bookingToDelete.eventType?.metadata || null | ||
| ); | ||
| throw new Error("Error parsing metadata"); | ||
| } | ||
|
|
||
| const bookingToDeleteEventTypeMetadata = bookingToDeleteEventTypeMetadataParsed.data; | ||
| if (!bookingToDeleteEventTypeMetadataParsed.success) { | ||
| log.error( | ||
| `Error parsing metadata`, | ||
| safeStringify({ error: bookingToDeleteEventTypeMetadataParsed?.error }) | ||
| ); | ||
| throw new Error("Error parsing metadata"); | ||
| } | ||
|
|
||
| const credentials = await getAllCredentialsIncludeServiceAccountKey(bookingToDelete.user, { | ||
| ...bookingToDelete.eventType, | ||
| metadata: bookingToDeleteEventTypeMetadata, | ||
| }); | ||
| const bookingToDeleteEventTypeMetadata = bookingToDeleteEventTypeMetadataParsed.data; | ||
|
|
||
| const eventManager = new EventManager( | ||
| { ...bookingToDelete.user, credentials }, | ||
| bookingToDeleteEventTypeMetadata?.apps | ||
| ); | ||
| const credentials = await getAllCredentialsIncludeServiceAccountKey(bookingToDelete.user, { | ||
| ...bookingToDelete.eventType, | ||
| metadata: bookingToDeleteEventTypeMetadata, | ||
| }); | ||
|
|
||
| const eventManager = new EventManager( | ||
| { ...bookingToDelete.user, credentials }, | ||
| bookingToDeleteEventTypeMetadata?.apps | ||
| ); | ||
|
|
||
| await eventManager.cancelEvent(evt, bookingToDelete.references, isBookingInRecurringSeries); | ||
| await eventManager.cancelEvent(evt, bookingToDelete.references, isBookingInRecurringSeries); | ||
| } catch (error) { | ||
| log.error(`Error deleting integrations`, safeStringify({ error })); | ||
| } | ||
| } | ||
|
|
||
| // Always mark booking references as deleted for data consistency | ||
| // (even when skipCalendarSyncTaskCancellation is true, since the external event is already deleted) | ||
| try { | ||
| await bookingReferenceRepository.updateManyByBookingId(bookingToDelete.id, { deleted: true }); | ||
| } catch (error) { | ||
| log.error(`Error deleting integrations`, safeStringify({ error })); | ||
| log.error(`Error marking booking references as deleted`, safeStringify({ error })); | ||
| } | ||
|
Comment on lines
+648
to
654
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When Suggested fix // When cancellation originates from a calendar sync webhook only the triggering
// integration's event is already gone; mark all references deleted only when
// we are the source of truth (i.e. not a passthrough skip).
if (!skipCalendarSyncTaskCancellation) {
try {
await bookingReferenceRepository.updateManyByBookingId(bookingToDelete.id, { deleted: true });
} catch (error) {
log.error(`Error marking booking references as deleted`, safeStringify({ error }));
}
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM:
Comment on lines
+650
to
654
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the original code, Suggested fix if (skipCalendarSyncTaskCancellation) {
// Sync path: external event is already deleted on the provider calendar;
// always mark references deleted for consistency.
try {
await bookingReferenceRepository.updateManyByBookingId(bookingToDelete.id, { deleted: true });
} catch (error) {
log.error(`Error marking booking references as deleted`, safeStringify({ error }));
}
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM:
Comment on lines
+650
to
654
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The refactor separates Suggested fix // When skipping calendar event deletion the external event was already removed;
// mark references deleted for consistency. When NOT skipping, only mark
// references deleted after a successful cancelEvent.
if (skipCalendarSyncTaskCancellation) {
try {
await bookingReferenceRepository.updateManyByBookingId(bookingToDelete.id, { deleted: true });
} catch (error) {
log.error(`Error marking booking references as deleted`, safeStringify({ error }));
}
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
|
|
||
| try { | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix for (const e of calEvents) {
if (e.status === "cancelled") {
await this.cancelBooking(e);
} else {
await this.rescheduleBooking(e);
}
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,10 @@ | ||||||||||||||
| import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; | ||||||||||||||
| import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking"; | ||||||||||||||
| import type { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; | ||||||||||||||
| import type { CalendarSubscriptionEventItem } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionPort.interface"; | ||||||||||||||
| import logger from "@calcom/lib/logger"; | ||||||||||||||
| import { safeStringify } from "@calcom/lib/safeStringify"; | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
import { metrics } from "@sentry/nextjs";
Fix: Use the framework-agnostic |
||||||||||||||
| import type { SelectedCalendar } from "@calcom/prisma/client"; | ||||||||||||||
| import { metrics } from "@sentry/nextjs"; | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||||||||||
|
|
||||||||||||||
| const log = logger.getSubLogger({ prefix: ["CalendarSyncService"] }); | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -31,10 +34,23 @@ export class CalendarSyncService { | |||||||||||||
| countEvents: calendarSubscriptionEvents.length, | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| metrics.count("calendar.sync.handleEvents.calls", 1, { | ||||||||||||||
| attributes: { | ||||||||||||||
| integration: selectedCalendar.integration, | ||||||||||||||
| }, | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| // only process cal.com calendar events | ||||||||||||||
| const calEvents = calendarSubscriptionEvents.filter((e) => | ||||||||||||||
| e.iCalUID?.toLowerCase()?.endsWith("@cal.com") | ||||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| metrics.distribution("calendar.sync.handleEvents.events_count", calEvents.length, { | ||||||||||||||
| attributes: { | ||||||||||||||
| integration: selectedCalendar.integration, | ||||||||||||||
| }, | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| if (calEvents.length === 0) { | ||||||||||||||
| log.debug("handleEvents: no calendar events to process"); | ||||||||||||||
| return; | ||||||||||||||
|
|
@@ -59,40 +75,136 @@ export class CalendarSyncService { | |||||||||||||
| * @returns | ||||||||||||||
| */ | ||||||||||||||
| async cancelBooking(event: CalendarSubscriptionEventItem) { | ||||||||||||||
| const startTime = performance.now(); | ||||||||||||||
| log.debug("cancelBooking", { event }); | ||||||||||||||
| const [bookingUid] = event.iCalUID?.split("@") ?? [undefined]; | ||||||||||||||
| if (!bookingUid) { | ||||||||||||||
| log.debug("Unable to sync, booking not found"); | ||||||||||||||
| log.debug("Unable to sync, booking UID not found in iCalUID"); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const booking = await this.deps.bookingRepository.findBookingByUidWithEventType({ bookingUid }); | ||||||||||||||
| if (!booking) { | ||||||||||||||
| log.debug("Unable to sync, booking not found"); | ||||||||||||||
| log.debug("Unable to sync, booking not found in database", { bookingUid }); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
86
to
90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Suggested fix let booking;
try {
booking = await this.deps.bookingRepository.findBookingByUidWithEventType({ bookingUid });
} catch (error) {
log.error("Unable to sync, failed to fetch booking from database", {
bookingUid,
error: safeStringify(error),
});
return;
}
if (!booking) {
log.debug("Unable to sync, booking not found in database", { bookingUid });
return;
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||||||||||
|
|
||||||||||||||
| // todo handle cancel booking | ||||||||||||||
| if (!booking.userId || !booking.userPrimaryEmail) { | ||||||||||||||
| log.warn("Unable to sync cancellation, booking missing required user data", { | ||||||||||||||
| bookingUid, | ||||||||||||||
| hasUserId: !!booking.userId, | ||||||||||||||
| hasUserPrimaryEmail: !!booking.userPrimaryEmail, | ||||||||||||||
| }); | ||||||||||||||
| metrics.count("calendar.sync.cancelBooking.calls", 1, { | ||||||||||||||
| attributes: { status: "skipped", reason: "missing_user_data" }, | ||||||||||||||
| }); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
86
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The code looks up a booking by UID from the iCalUID but never verifies that |
||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| await handleCancelBooking({ | ||||||||||||||
| userId: booking.userId, | ||||||||||||||
|
Comment on lines
+103
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
cancelledBy: booking.userPrimaryEmail,
Consider using a dedicated system identifier (e.g. |
||||||||||||||
| bookingData: { | ||||||||||||||
| uid: booking.uid, | ||||||||||||||
| cancellationReason: "Cancelled on user's calendar", | ||||||||||||||
|
Comment on lines
90
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The guard on line 90 checks only that Suggested fix const isValidEmail = (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
if (!booking.userId || !booking.userPrimaryEmail || !isValidEmail(booking.userPrimaryEmail)) {
log.warn("Unable to sync cancellation, booking missing required user data", {
bookingUid,
hasUserId: !!booking.userId,
hasUserPrimaryEmail: !!booking.userPrimaryEmail,
});
metrics.count("calendar.sync.cancelBooking.calls", 1, {
attributes: { status: "skipped", reason: "missing_user_data" },
});
return;
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||||||||||
| cancelledBy: booking.userPrimaryEmail, | ||||||||||||||
| // Skip calendar event deletion to avoid infinite loops | ||||||||||||||
| // (Google/Office365 → Cal.com → Google/Office365 → ...) | ||||||||||||||
| skipCalendarSyncTaskCancellation: true, | ||||||||||||||
| }, | ||||||||||||||
| }); | ||||||||||||||
|
Comment on lines
+105
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix await handleCancelBooking({
userId: booking.userId,
actionSource: "WEBHOOK",
bookingData: {
uid: booking.uid,
cancellationReason: "Cancelled on user's calendar",
cancelledBy: booking.userPrimaryEmail,
// Skip calendar event deletion to avoid infinite loops
// (Google/Office365 → Cal.com → Google/Office365 → ...)
skipCalendarSyncTaskCancellation: true,
},
});Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM:
Comment on lines
+104
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix await handleCancelBooking({
userId: booking.userId,
actionSource: "WEBHOOK",
bookingData: {
uid: booking.uid,
cancellationReason: "Cancelled on user's calendar",
cancelledBy: booking.userPrimaryEmail,
// Skip calendar event deletion to avoid infinite loops
// (Google/Office365 → Cal.com → Google/Office365 → ...)
skipCalendarSyncTaskCancellation: true,
},
});Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||||||||||
| log.info("Successfully cancelled booking from calendar sync", { bookingUid }); | ||||||||||||||
|
|
||||||||||||||
| metrics.count("calendar.sync.cancelBooking.calls", 1, { | ||||||||||||||
| attributes: { status: "success" }, | ||||||||||||||
| }); | ||||||||||||||
| metrics.distribution("calendar.sync.cancelBooking.duration_ms", performance.now() - startTime, { | ||||||||||||||
| attributes: { status: "success" }, | ||||||||||||||
| }); | ||||||||||||||
| } catch (error) { | ||||||||||||||
| // Log error but don't block - calendar change should still be reflected | ||||||||||||||
| log.error("Failed to cancel booking from calendar sync", { bookingUid, error: safeStringify(error) }); | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested fix const isExpectedClientError =
typeof (error as { statusCode?: number })?.statusCode === "number" &&
(error as { statusCode: number }).statusCode >= 400 &&
(error as { statusCode: number }).statusCode < 500;
if (isExpectedClientError) {
log.warn("Calendar sync cancellation skipped due to booking validation", {
bookingUid,
message: String((error as Error).message),
});
} else {
log.error("Failed to cancel booking from calendar sync", { bookingUid, error: safeStringify(error) });
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM:
Comment on lines
+124
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||||||||||
|
|
||||||||||||||
| metrics.count("calendar.sync.cancelBooking.calls", 1, { | ||||||||||||||
| attributes: { status: "error" }, | ||||||||||||||
| }); | ||||||||||||||
| metrics.distribution("calendar.sync.cancelBooking.duration_ms", performance.now() - startTime, { | ||||||||||||||
| attributes: { status: "error" }, | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Reschedule a booking | ||||||||||||||
| * @param event | ||||||||||||||
| */ | ||||||||||||||
| async rescheduleBooking(event: CalendarSubscriptionEventItem) { | ||||||||||||||
| const startTime = performance.now(); | ||||||||||||||
| log.debug("rescheduleBooking", { event }); | ||||||||||||||
| const [bookingUid] = event.iCalUID?.split("@") ?? [undefined]; | ||||||||||||||
| if (!bookingUid) { | ||||||||||||||
| log.debug("Unable to sync, booking not found"); | ||||||||||||||
| log.debug("Unable to sync, booking UID not found in iCalUID"); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const booking = await this.deps.bookingRepository.findBookingByUidWithEventType({ bookingUid }); | ||||||||||||||
| if (!booking) { | ||||||||||||||
| log.debug("Unable to sync, booking not found"); | ||||||||||||||
| log.debug("Unable to sync, booking not found in database", { bookingUid }); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if (!booking.eventTypeId) { | ||||||||||||||
| log.warn("Unable to sync reschedule, booking missing eventTypeId", { bookingUid }); | ||||||||||||||
| metrics.count("calendar.sync.rescheduleBooking.calls", 1, { | ||||||||||||||
| attributes: { status: "skipped", reason: "missing_event_type_id" }, | ||||||||||||||
| }); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // todo handle update booking | ||||||||||||||
| try { | ||||||||||||||
| // Dynamic import to avoid loading the entire booking service chain at module evaluation time | ||||||||||||||
| // This prevents react-awesome-query-builder from being loaded in server-side contexts | ||||||||||||||
| const { getRegularBookingService } = await import( | ||||||||||||||
| "@calcom/features/bookings/di/RegularBookingService.container" | ||||||||||||||
|
Comment on lines
+155
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The infinite-loop guard for reschedule depends on Fix: Confirm (and include in this PR) the code path inside |
||||||||||||||
| ); | ||||||||||||||
| const regularBookingService = getRegularBookingService(); | ||||||||||||||
| await regularBookingService.createBooking({ | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent timeZone: event.timeZone ?? "UTC",If the incoming calendar event has no Fix: fall back to the booking's original timezone (if available from the repository result), or surface this as a validation error and skip the reschedule with an appropriate log/metric instead of silently accepting a wrong timezone. |
||||||||||||||
| bookingData: { | ||||||||||||||
| eventTypeId: booking.eventTypeId, | ||||||||||||||
| start: event.start?.toISOString() ?? booking.startTime.toISOString(), | ||||||||||||||
| end: event.end?.toISOString() ?? booking.endTime.toISOString(), | ||||||||||||||
| timeZone: event.timeZone ?? "UTC", | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||||||||||
| language: "en", | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||||||||||||||
| metadata: {}, | ||||||||||||||
| rescheduleUid: booking.uid, | ||||||||||||||
| }, | ||||||||||||||
| bookingMeta: { | ||||||||||||||
| // Skip calendar event creation to avoid infinite loops | ||||||||||||||
| // (Google/Office365 → Cal.com → Google/Office365 → ...) | ||||||||||||||
| skipCalendarSyncTaskCreation: true, | ||||||||||||||
| }, | ||||||||||||||
| }); | ||||||||||||||
| log.info("Successfully rescheduled booking from calendar sync", { bookingUid }); | ||||||||||||||
|
|
||||||||||||||
| metrics.count("calendar.sync.rescheduleBooking.calls", 1, { | ||||||||||||||
| attributes: { status: "success" }, | ||||||||||||||
| }); | ||||||||||||||
| metrics.distribution("calendar.sync.rescheduleBooking.duration_ms", performance.now() - startTime, { | ||||||||||||||
| attributes: { status: "success" }, | ||||||||||||||
| }); | ||||||||||||||
| } catch (error) { | ||||||||||||||
| // Log error but don't block - calendar change should still be reflected | ||||||||||||||
| log.error("Failed to reschedule booking from calendar sync", { | ||||||||||||||
| bookingUid, | ||||||||||||||
| error: safeStringify(error), | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| metrics.count("calendar.sync.rescheduleBooking.calls", 1, { | ||||||||||||||
| attributes: { status: "error" }, | ||||||||||||||
| }); | ||||||||||||||
| metrics.distribution("calendar.sync.rescheduleBooking.duration_ms", performance.now() - startTime, { | ||||||||||||||
| attributes: { status: "error" }, | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bookingReferenceRepository.updateManyByBookingIdnow runs even whencancelEventfailsBefore this PR both the
cancelEventcall andupdateManyByBookingIdsat inside the sametryblock, so references were only marked deleted when the calendar event was actually removed. The refactor pullsupdateManyByBookingIdinto a separate, unconditionaltryblock. On the non-skip path, ifeventManager.cancelEventthrows, the error is swallowed by the firstcatch, and then the reference rows are still markeddeleted: trueeven though the external calendar event was never cancelled. This leaves the database in an inconsistent state (references say deleted, calendar still has the event).Fix: only mark references deleted after a successful
cancelEventcall (restore both operations to the sametryblock on the normal path), or explicitly check whethercancelEventsucceeded before marking references.