Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
78ba17e
feat: calendar cache and sync - wip
volnei Sep 16, 2025
6b6d00d
Add env.example
volnei Sep 16, 2025
79de50b
refactor on CalendarCacheEventService
volnei Sep 16, 2025
11d1bd2
remove test console.log
volnei Sep 16, 2025
fc5b3f9
Fix type checks errors
volnei Sep 16, 2025
4daf6d6
chore: remove pt comment
volnei Sep 16, 2025
f027bfc
add route.ts
volnei Sep 16, 2025
118a88a
chore: fix tests
volnei Sep 16, 2025
47a231d
Improve cache impl
volnei Sep 17, 2025
da76f39
chore: update recurring event id
volnei Sep 17, 2025
c6b45ea
chore: small improvements
volnei Sep 17, 2025
55d484b
calendar cache improvements
volnei Sep 18, 2025
36eadd9
Merge branch 'main' into feat/calendar_sync_cache
volnei Sep 18, 2025
1d2e0f1
Fix remove dynamic imports
volnei Sep 18, 2025
c5f8fab
Merge branch 'feat/calendar_sync_cache' of https://github.com/calcom/…
volnei Sep 18, 2025
739e3bc
Add cleanup stale cache
volnei Sep 18, 2025
d9891c1
Fix tests
volnei Sep 18, 2025
e325552
add event update
volnei Sep 18, 2025
72f30ae
type fixes
volnei Sep 18, 2025
088aa3f
feat: add comprehensive tests for new calendar subscription API routes
devin-ai-integration[bot] Sep 19, 2025
10b8607
feat: add comprehensive tests for calendar subscription services, rep…
devin-ai-integration[bot] Sep 19, 2025
14a8c6a
fix: improve calendar-subscriptions-cleanup test performance by addin…
devin-ai-integration[bot] Sep 19, 2025
b6dc0bf
Fix tests
volnei Sep 19, 2025
30b2ad2
Fix tests
volnei Sep 19, 2025
a683faa
type fix
volnei Sep 19, 2025
b486347
Fix coderabbit comments
volnei Sep 19, 2025
075e055
Fix types
volnei Sep 19, 2025
b1ef7b6
Fix test
volnei Sep 19, 2025
0b8d5d6
Merge branch 'main' into feat/calendar_sync_cache
volnei Sep 20, 2025
7c267ad
Update apps/web/app/api/cron/calendar-subscriptions/route.ts
volnei Sep 20, 2025
5e8ed4f
Fixes by first review
volnei Sep 20, 2025
2605eee
merge conflict
volnei Sep 20, 2025
68bde1e
feat: add database migrations for calendar cache and sync fields
devin-ai-integration[bot] Sep 22, 2025
c556e66
only google-calendar for now
volnei Sep 23, 2025
6066bf0
Merge branch 'main' into feat/calendar_sync_cache
volnei Sep 23, 2025
d34b878
docs: add Calendar Cache and Sync feature documentation
devin-ai-integration[bot] Sep 23, 2025
f147197
docs: update calendar subscription README with comprehensive document…
devin-ai-integration[bot] Sep 23, 2025
a967b42
fix docs
volnei Sep 23, 2025
95bf75f
Merge branch 'main' into feat/calendar_sync_cache
keithwillcode Sep 23, 2025
fdbf170
Fix test to available calendars
volnei Sep 24, 2025
43e892d
Fix test to available calendars
volnei Sep 24, 2025
35b4f6b
Merge branch 'main' into feat/calendar_sync_cache
volnei Sep 24, 2025
5238b0d
add migration and sync boilerplate
volnei Sep 24, 2025
153a85e
fix typo
volnei Sep 25, 2025
f707d1e
remove double log
volnei Sep 25, 2025
a251399
sync boilerplate
volnei Sep 25, 2025
57c1133
Merge branch 'main' into feat/calendar_sync_cache
volnei Sep 25, 2025
87d4e0d
remove console.log
volnei Sep 25, 2025
cf10b33
only subscribe for google calendar
volnei Sep 25, 2025
81ffd24
adjust for 3 months fetch
volnei Sep 26, 2025
9d514ab
Merge branch 'main' into feat/calendar_sync_cache
volnei Sep 26, 2025
0b887e1
only subscribe for teams that have feature enabled
volnei Sep 26, 2025
893d1e5
adjust tests
volnei Sep 26, 2025
1e430fe
chore: safe increment error count
volnei Sep 27, 2025
3ef231f
calendar sync
volnei Sep 27, 2025
3928bbc
Merge branch 'main' into feat/calendar_sync_cache
volnei Sep 27, 2025
032dbed
Merge branch 'feat/calendar_sync_cache' into feat/calendar_sync
volnei Sep 27, 2025
68e26b1
test: add comprehensive tests for CalendarSyncService
devin-ai-integration[bot] Sep 27, 2025
23fbfba
Merge branch 'main' into feat/calendar_sync
volnei Sep 29, 2025
241c46a
Merge branch 'main' into feat/calendar_sync
volnei Oct 10, 2025
fcbddb3
Merge branch 'main' into feat/calendar_sync
volnei Dec 21, 2025
0d25b29
fix: add skipCalendarSyncTaskCreation flag to handleCancelBooking to …
devin-ai-integration[bot] Dec 22, 2025
4458e35
refactor: rename flag to skipCalendarSyncTaskCancellation and use sta…
devin-ai-integration[bot] Dec 22, 2025
b2f2a03
feat: add error handling and static import for handleNewBooking
devin-ai-integration[bot] Dec 22, 2025
1bddf12
Merge branch 'main' into feat/calendar_sync
volnei Dec 22, 2025
34c7abb
refactor: use RegularBookingService directly and fix safeStringify usage
devin-ai-integration[bot] Dec 22, 2025
8ef1e6c
fix: restore handleNewBooking/index.ts and fix bookingData structure …
devin-ai-integration[bot] Dec 22, 2025
2dc1985
refactor: address PR comments - use RegularBookingService directly, r…
devin-ai-integration[bot] Dec 22, 2025
327e9f4
chore: remove unnecessary comment from zod-utils.ts
devin-ai-integration[bot] Dec 22, 2025
9bb49b7
Merge branch 'main' into feat/calendar_sync
volnei Dec 24, 2025
d8a72b8
Merge branch 'main' into feat/calendar_sync
devin-ai-integration[bot] Jan 20, 2026
946e9ea
Merge branch 'main' into feat/calendar_sync
volnei Jan 20, 2026
6cb5e3b
feat: add Sentry metrics telemetry and fix null assertions in Calenda…
devin-ai-integration[bot] Jan 20, 2026
b75e507
fix: use dynamic import for getRegularBookingService to avoid RAQB im…
devin-ai-integration[bot] Jan 20, 2026
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
55 changes: 33 additions & 22 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Comment on lines +643 to +648

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bookingReferenceRepository.updateManyByBookingId now runs even when cancelEvent fails

Before this PR both the cancelEvent call and updateManyByBookingId sat inside the same try block, so references were only marked deleted when the calendar event was actually removed. The refactor pulls updateManyByBookingId into a separate, unconditional try block. On the non-skip path, if eventManager.cancelEvent throws, the error is swallowed by the first catch, and then the reference rows are still marked deleted: true even though the external calendar event was never cancelled. This leaves the database in an inconsistent state (references say deleted, calendar still has the event).

// Problem: first catch swallows cancelEvent failure, then this always runs
try {
  await bookingReferenceRepository.updateManyByBookingId(bookingToDelete.id, { deleted: true });
} catch (error) {  }

Fix: only mark references deleted after a successful cancelEvent call (restore both operations to the same try block on the normal path), or explicitly check whether cancelEvent succeeded before marking references.

// (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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional High

When skipCalendarSyncTaskCancellation is true (i.e. the cancellation was triggered by a single calendar's webhook), the code still calls bookingReferenceRepository.updateManyByBookingId(bookingToDelete.id, { deleted: true }) which marks all booking references — across every connected calendar integration — as deleted. If the user has both Google Calendar and Outlook connected, only the Google Calendar event was actually deleted externally; the Outlook event remains active, but its DB reference is now marked deleted. Cal.com will never attempt to clean up that orphaned Outlook event, leaving the two systems permanently out of sync. The references for integrations that were NOT the source of the webhook should not be marked deleted unless their events are actually cancelled.

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 assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert typescript developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/bookings/lib/handleCancelBooking.ts
Lines: 648-654
Issue Type: functional-high
Severity: high

Issue Description:
When `skipCalendarSyncTaskCancellation` is `true` (i.e. the cancellation was triggered by a single calendar's webhook), the code still calls `bookingReferenceRepository.updateManyByBookingId(bookingToDelete.id, { deleted: true })` which marks **all** booking references — across every connected calendar integration — as deleted. If the user has both Google Calendar and Outlook connected, only the Google Calendar event was actually deleted externally; the Outlook event remains active, but its DB reference is now marked deleted. Cal.com will never attempt to clean up that orphaned Outlook event, leaving the two systems permanently out of sync. The references for integrations that were NOT the source of the webhook should not be marked deleted unless their events are actually cancelled.

Current Code:
  // 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 marking booking references as deleted`, safeStringify({ error }));
  }

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow typescript best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue

Comment on lines +650 to 654

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional Medium

In the original code, bookingReferenceRepository.updateManyByBookingId(..., { deleted: true }) lived inside the same try block as eventManager.cancelEvent. If cancelEvent threw (e.g. network timeout, OAuth revocation), the catch swallowed the error and references were NOT marked deleted, leaving them in a state where a retry could re-attempt the calendar API call. This refactor moves reference deletion into a separate always-executed try/catch, so references are now marked deleted: true even when cancelEvent fails. The comment at line 649 justifies this only for the sync path ("external event is already deleted"), but the block also runs on the normal cancellation path where the calendar event may genuinely still exist on the provider. A failed calendar delete followed by a reference being marked deleted creates a phantom orphaned calendar event that cannot be cleaned up through normal retry paths. The behavioural change should either be explicitly bounded to skipCalendarSyncTaskCancellation === true, or the comment should acknowledge and justify the normal-path change.

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 assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert bash developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/bookings/lib/handleCancelBooking.ts
Lines: 650-654
Issue Type: functional-medium
Severity: medium

Issue Description:
In the original code, `bookingReferenceRepository.updateManyByBookingId(..., { deleted: true })` lived inside the same try block as `eventManager.cancelEvent`. If `cancelEvent` threw (e.g. network timeout, OAuth revocation), the catch swallowed the error and references were NOT marked deleted, leaving them in a state where a retry could re-attempt the calendar API call. This refactor moves reference deletion into a separate always-executed try/catch, so references are now marked `deleted: true` even when `cancelEvent` fails. The comment at line 649 justifies this only for the sync path ("external event is already deleted"), but the block also runs on the normal cancellation path where the calendar event may genuinely still exist on the provider. A failed calendar delete followed by a reference being marked deleted creates a phantom orphaned calendar event that cannot be cleaned up through normal retry paths. The behavioural change should either be explicitly bounded to `skipCalendarSyncTaskCancellation === true`, or the comment should acknowledge and justify the normal-path change.

Current Code:
  try {
    await bookingReferenceRepository.updateManyByBookingId(bookingToDelete.id, { deleted: true });
  } catch (error) {
    log.error(`Error marking booking references as deleted`, safeStringify({ error }));
  }

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow bash best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

Comment on lines +650 to 654

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional High

The refactor separates bookingReferenceRepository.updateManyByBookingId from the eventManager.cancelEvent try-block, so it now runs unconditionally even when cancelEvent throws. In the original code both calls lived in the same try-block: if cancelEvent failed the references were NOT marked deleted, keeping the DB consistent with the external calendar state. Now, if cancelEvent fails on the normal (non-skip) path, the booking references in cal.com are marked deleted while the actual calendar event may still exist: creating a silent inconsistency that is very hard to detect or reconcile later.

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 assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert bash developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/bookings/lib/handleCancelBooking.ts
Lines: 650-654
Issue Type: functional-high
Severity: high

Issue Description:
The refactor separates `bookingReferenceRepository.updateManyByBookingId` from the `eventManager.cancelEvent` try-block, so it now runs unconditionally even when `cancelEvent` throws. In the original code both calls lived in the same try-block: if `cancelEvent` failed the references were NOT marked deleted, keeping the DB consistent with the external calendar state. Now, if `cancelEvent` fails on the normal (non-skip) path, the booking references in cal.com are marked deleted while the actual calendar event may still exist: creating a silent inconsistency that is very hard to detect or reconcile later.

Current Code:
  // 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 marking booking references as deleted`, safeStringify({ error }));
  }

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow bash best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira


try {
Expand Down

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance Medium

Promise.all fans out all filtered Cal.com events concurrently with no concurrency cap. A calendar diff with 50: 100 changed events (e.g. after a long offline period or a full re-sync) launches that many simultaneous handleCancelBooking or createBooking calls, each of which opens multiple Prisma queries, sends emails, fires webhooks, and calls third-party calendar APIs. This can exhaust the Prisma connection pool, trigger rate-limits on downstream services, and spike DB load for every subscriber that syncs at the same time. Sequential processing or a bounded concurrency limit (e.g. p-limit with 5 or less) would keep throughput acceptable while protecting downstream resources.

Suggested fix
    for (const e of calEvents) {
      if (e.status === "cancelled") {
        await this.cancelBooking(e);
      } else {
        await this.rescheduleBooking(e);
      }
    }
Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert bash developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 61-69
Issue Type: performance-medium
Severity: medium

Issue Description:
`Promise.all` fans out all filtered Cal.com events concurrently with no concurrency cap. A calendar diff with 50: 100 changed events (e.g. after a long offline period or a full re-sync) launches that many simultaneous `handleCancelBooking` or `createBooking` calls, each of which opens multiple Prisma queries, sends emails, fires webhooks, and calls third-party calendar APIs. This can exhaust the Prisma connection pool, trigger rate-limits on downstream services, and spike DB load for every subscriber that syncs at the same time. Sequential processing or a bounded concurrency limit (e.g. p-limit with 5 or less) would keep throughput acceptable while protecting downstream resources.

Current Code:
    await Promise.all(
      calEvents.map((e) => {
        if (e.status === "cancelled") {
          return this.cancelBooking(e);
        } else {
          return this.rescheduleBooking(e);
        }
      })
    );

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow bash best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sentry/nextjs imported directly in a platform-agnostic feature package

import { metrics } from "@sentry/nextjs";

packages/features/calendar-subscription is a shared feature library that should be framework-agnostic. Importing from @sentry/nextjs (a Next.js-specific SDK) couples this package to the Next.js runtime and will break if the service is ever run outside of Next.js (e.g. a standalone worker, a different framework, or unit tests that don't mock the module).

Fix: Use the framework-agnostic @sentry/node (or @sentry/core) package, or abstract the metrics behind an injected interface so callers can provide their own telemetry implementation.

import type { SelectedCalendar } from "@calcom/prisma/client";
import { metrics } from "@sentry/nextjs";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 @sentry/nextjs imported in a shared library package

CalendarSyncService lives in packages/features/calendar-subscription, which may be consumed by workers or cron jobs outside a Next.js context. @sentry/nextjs has a Next.js-specific initialisation contract and may fail in other runtimes. Prefer @sentry/core for the metrics API.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainability High

@sentry/nextjs is a Next.js-specific Sentry SDK imported into a shared feature package (packages/features/). Any consumer that runs outside a Next.js context — queue workers, CLI scripts, non-Next serverless runtimes, or plain Node.js cron jobs — will either fail at import time or emit runtime errors because the Next.js instrumentation hooks Sentry relies on are absent. The correct package for shared/server code is @sentry/node (or the framework-agnostic @sentry/core). Additionally, Sentry metrics should ideally be injected as a dependency rather than hard-imported so this service remains testable without Sentry initialisation.

Suggested change
import { metrics } from "@sentry/nextjs";
import { metrics } from "@sentry/node";
Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert typescript developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 7-7
Issue Type: maintainability-high
Severity: high

Issue Description:
`@sentry/nextjs` is a Next.js-specific Sentry SDK imported into a shared feature package (`packages/features/`). Any consumer that runs outside a Next.js context — queue workers, CLI scripts, non-Next serverless runtimes, or plain Node.js cron jobs — will either fail at import time or emit runtime errors because the Next.js instrumentation hooks Sentry relies on are absent. The correct package for shared/server code is `@sentry/node` (or the framework-agnostic `@sentry/core`). Additionally, Sentry metrics should ideally be injected as a dependency rather than hard-imported so this service remains testable without Sentry initialisation.

Current Code:
import { metrics } from "@sentry/nextjs";

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow typescript best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainability Medium

CalendarSyncService lives in a shared packages/features package but statically imports { metrics } from @sentry/nextjs, a Next.js-specific SDK. This hard-couples the module to the Next.js runtime and will fail to initialize correctly (or outright throw) when the package is loaded in a standalone Node.js server, a background worker, or any non-Next.js consumer. The same file already avoids this problem for RegularBookingService.container via a dynamic import() — the Sentry metrics import should follow the same pattern or use the provider-agnostic @sentry/core package.

Suggested change
import { metrics } from "@sentry/nextjs";
import { metrics } from "@sentry/core";
Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert typescript developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 7-7
Issue Type: maintainability-medium
Severity: medium

Issue Description:
`CalendarSyncService` lives in a shared `packages/features` package but statically imports `{ metrics }` from `@sentry/nextjs`, a Next.js-specific SDK. This hard-couples the module to the Next.js runtime and will fail to initialize correctly (or outright throw) when the package is loaded in a standalone Node.js server, a background worker, or any non-Next.js consumer. The same file already avoids this problem for `RegularBookingService.container` via a dynamic `import()` — the Sentry metrics import should follow the same pattern or use the provider-agnostic `@sentry/core` package.

Current Code:
import { metrics } from "@sentry/nextjs";

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow typescript best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainability Medium

CalendarSyncService lives in packages/features/calendar-subscription/, a shared feature package that is (or may be) consumed from workers, cron jobs, or other non-Next.js runtimes. Importing { metrics } from @sentry/nextjs rather than a framework-agnostic package like @sentry/core couples this service to the Next.js Sentry integration. @sentry/nextjs monkey-patches Next.js internals at import time and may fail or behave unpredictably outside a Next.js request context. Using the framework-neutral @sentry/core (which @sentry/nextjs re-exports for this purpose) removes the runtime constraint.

Suggested change
import { metrics } from "@sentry/nextjs";
import { metrics } from "@sentry/core";
Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert bash developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 7-7
Issue Type: maintainability-medium
Severity: medium

Issue Description:
`CalendarSyncService` lives in `packages/features/calendar-subscription/`, a shared feature package that is (or may be) consumed from workers, cron jobs, or other non-Next.js runtimes. Importing `{ metrics }` from `@sentry/nextjs` rather than a framework-agnostic package like `@sentry/core` couples this service to the Next.js Sentry integration. `@sentry/nextjs` monkey-patches Next.js internals at import time and may fail or behave unpredictably outside a Next.js request context. Using the framework-neutral `@sentry/core` (which `@sentry/nextjs` re-exports for this purpose) removes the runtime constraint.

Current Code:
import { metrics } from "@sentry/nextjs";

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow bash best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira


const log = logger.getSubLogger({ prefix: ["CalendarSyncService"] });

Expand Down Expand Up @@ -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;
Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness Medium

The bookingRepository.findBookingByUidWithEventType call in cancelBooking (and its mirror in rescheduleBooking at line ~152) sits outside the try/catch block. When this DB call throws — e.g. on a transient connection error — the exception propagates up to Promise.all in handleEvents. Promise.all fails fast on the first rejection, so any other calendar events in the same batch that haven't started yet are silently skipped. A single flaky DB call can therefore cause an entire sync batch to be partially applied with no retry and no per-event error logged.

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 assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert typescript developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 86-90
Issue Type: robustness-medium
Severity: medium

Issue Description:
The `bookingRepository.findBookingByUidWithEventType` call in `cancelBooking` (and its mirror in `rescheduleBooking` at line ~152) sits outside the try/catch block. When this DB call throws — e.g. on a transient connection error — the exception propagates up to `Promise.all` in `handleEvents`. `Promise.all` fails fast on the first rejection, so any other calendar events in the same batch that haven't started yet are silently skipped. A single flaky DB call can therefore cause an entire sync batch to be partially applied with no retry and no per-event error logged.

Current Code:
    const booking = await this.deps.bookingRepository.findBookingByUidWithEventType({ bookingUid });
    if (!booking) {
      log.debug("Unable to sync, booking not found in database", { bookingUid });
      return;
    }

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow typescript best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue


// 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 security Missing booking ownership check against the calendar user

The code looks up a booking by UID from the iCalUID but never verifies that booking.userId === selectedCalendar.userId before cancelling or rescheduling. If an attacker can inject a cal.com-formatted iCalUID (e.g., victim-booking-uid@cal.com) into any subscribed calendar — via a shared calendar, calendar import, or a compromised calendar account — handleCancelBooking will be called with userId: booking.userId (the victim's ID), authorizing the cancellation as if the victim requested it.


try {
await handleCancelBooking({
userId: booking.userId,
Comment on lines +103 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancelledBy is set to the booking owner's email, not the cancelling party

cancelledBy: booking.userPrimaryEmail,

booking.userPrimaryEmail is the calendar owner / organiser. In a calendar-sync cancellation the triggering party could be an attendee who deleted the event on their calendar. Misattributing the cancellation to the organiser may surface incorrect audit trails and cancellation emails ("You have cancelled your own booking").

Consider using a dedicated system identifier (e.g. "calendar-sync@cal.com") or deriving the attendee's email from the selected calendar context when available.

bookingData: {
uid: booking.uid,
cancellationReason: "Cancelled on user's calendar",
Comment on lines 90 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness Medium

The guard on line 90 checks only that booking.userPrimaryEmail is truthy (!booking.userPrimaryEmail), but bookingCancelSchema (in zod-utils.ts) validates cancelledBy with z.string().email(). If any stored userPrimaryEmail value is a non-empty but malformed string (e.g. "pending", "<unknown>", or a display-name-only value), the Zod parse inside handleCancelBooking will throw a ZodError. That error is silently swallowed by the outer catch block and only logged, meaning the cancellation never actually propagates while the caller receives no signal of failure. The guard should validate email format before calling handleCancelBooking, or the schema should make cancelledBy accept any string from trusted internal callers.

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 assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert typescript developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 90-109
Issue Type: robustness-medium
Severity: medium

Issue Description:
The guard on line 90 checks only that `booking.userPrimaryEmail` is truthy (`!booking.userPrimaryEmail`), but `bookingCancelSchema` (in `zod-utils.ts`) validates `cancelledBy` with `z.string().email()`. If any stored `userPrimaryEmail` value is a non-empty but malformed string (e.g. `"pending"`, `"<unknown>"`, or a display-name-only value), the Zod parse inside `handleCancelBooking` will throw a `ZodError`. That error is silently swallowed by the outer `catch` block and only logged, meaning the cancellation never actually propagates while the caller receives no signal of failure. The guard should validate email format before calling `handleCancelBooking`, or the schema should make `cancelledBy` accept any string from trusted internal callers.

Current Code:
    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;
    }

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow typescript best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional Medium

handleCancelBooking is called without an actionSource, so it defaults to "UNKNOWN" inside the handler. The handler explicitly checks for this and emits log.warn("Booking cancellation with unknown actionSource", ...) on every single calendar-sync cancellation. "WEBHOOK" is the closest semantic match in the existing ActionSource enum (API_V1 | API_V2 | WEBAPP | WEBHOOK | MAGIC_LINK | UNKNOWN) and should be passed here so audit records are correctly attributed and the warning is not emitted.

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 assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert typescript developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 105-115
Issue Type: functional-medium
Severity: medium

Issue Description:
`handleCancelBooking` is called without an `actionSource`, so it defaults to `"UNKNOWN"` inside the handler. The handler explicitly checks for this and emits `log.warn("Booking cancellation with unknown actionSource", ...)` on every single calendar-sync cancellation. `"WEBHOOK"` is the closest semantic match in the existing `ActionSource` enum (`API_V1 | API_V2 | WEBAPP | WEBHOOK | MAGIC_LINK | UNKNOWN`) and should be passed here so audit records are correctly attributed and the warning is not emitted.

Current Code:
      await handleCancelBooking({
        userId: booking.userId,
        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,
        },
      });

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow typescript best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue

Comment on lines +104 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainability Medium

handleCancelBooking is called without actionSource, so every calendar-sync-triggered cancellation defaults to "UNKNOWN" and emits a log.warn("Booking cancellation with unknown actionSource", ...) (handleCancelBooking.ts:181-188). This produces permanent log noise for a legitimate, expected code path. The ActionSourceSchema enum (packages/features/booking-audit/lib/types/actionSource.ts) does not include a "CALENDAR_SYNC" value; "WEBHOOK" is the closest semantic match and would suppress the spurious warning.

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 assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert bash developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 104-115
Issue Type: maintainability-medium
Severity: medium

Issue Description:
`handleCancelBooking` is called without `actionSource`, so every calendar-sync-triggered cancellation defaults to `"UNKNOWN"` and emits a `log.warn("Booking cancellation with unknown actionSource", ...)` (handleCancelBooking.ts:181-188). This produces permanent log noise for a legitimate, expected code path. The `ActionSourceSchema` enum (`packages/features/booking-audit/lib/types/actionSource.ts`) does not include a `"CALENDAR_SYNC"` value; `"WEBHOOK"` is the closest semantic match and would suppress the spurious warning.

Current Code:
      await handleCancelBooking({
        userId: booking.userId,
        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,
        },
      });

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow bash best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

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) });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness Medium

handleCancelBooking throws HttpError (4xx) for several expected, non-fatal conditions that will commonly occur during calendar sync: "This booking has already been cancelled" (idempotent re-delivery of the same webhook), "Cannot cancel a booking that has already ended" (user deletes a past event), "This event type does not allow cancellations". All of these are caught here and emitted as log.error, indistinguishable from genuine system failures (DB down, unhandled exception). Operators monitoring error-level logs or Sentry issues will see constant noise from routine sync scenarios, masking real failures and causing alert fatigue. The catch should distinguish 4xx HttpError instances (log at warn/info) from actual system errors (log at error).

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 assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert bash developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 126-126
Issue Type: robustness-medium
Severity: medium

Issue Description:
`handleCancelBooking` throws `HttpError` (4xx) for several expected, non-fatal conditions that will commonly occur during calendar sync: "This booking has already been cancelled" (idempotent re-delivery of the same webhook), "Cannot cancel a booking that has already ended" (user deletes a past event), "This event type does not allow cancellations". All of these are caught here and emitted as `log.error`, indistinguishable from genuine system failures (DB down, unhandled exception). Operators monitoring error-level logs or Sentry issues will see constant noise from routine sync scenarios, masking real failures and causing alert fatigue. The catch should distinguish 4xx `HttpError` instances (log at warn/info) from actual system errors (log at error).

Current Code:
        log.error("Failed to cancel booking from calendar sync", { bookingUid, error: safeStringify(error) });

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow bash best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

Comment on lines +124 to +126

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness Medium

When handleCancelBooking throws for a legitimate business-logic reason: e.g. eventType.disableCancelling === true (HTTP 400), or the booking has already ended (HTTP 400): the error is caught, logged, and silently discarded. The result is a persistent split-brain state: the external calendar (Google/Office365) shows the event as cancelled, but the cal.com booking remains CONFIRMED in the database. There is no retry, alert, or dead-letter queue. Consider distinguishing between transient errors (worth retrying) and permanent business-rule rejections (worth surfacing via a metric or separate alert rather than simply swallowing).

Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert bash developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 124-126
Issue Type: robustness-medium
Severity: medium

Issue Description:
When `handleCancelBooking` throws for a legitimate business-logic reason: e.g. `eventType.disableCancelling === true` (HTTP 400), or the booking has already ended (HTTP 400): the error is caught, logged, and silently discarded. The result is a persistent split-brain state: the external calendar (Google/Office365) shows the event as cancelled, but the cal.com booking remains `CONFIRMED` in the database. There is no retry, alert, or dead-letter queue. Consider distinguishing between transient errors (worth retrying) and permanent business-rule rejections (worth surfacing via a metric or separate alert rather than simply swallowing).

Current Code:
      // 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) });

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow bash best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skipCalendarSyncTaskCreation flag passed to createBooking but not validated to be consumed

The infinite-loop guard for reschedule depends on bookingMeta.skipCalendarSyncTaskCreation: true being honoured by RegularBookingService.createBooking. However this flag is not introduced anywhere in this diff, so there is no guarantee it is actually read — making the guard a silent no-op. If the consumer ignores it, every calendar reschedule will fire a downstream calendar update, triggering another webhook and causing the exact infinite loop the flag is meant to prevent.

Fix: Confirm (and include in this PR) the code path inside createBooking/RegularBookingService that checks bookingMeta.skipCalendarSyncTaskCreation and skips the outbound calendar write, analogous to how skipCalendarSyncTaskCancellation is wired in handleCancelBooking.ts.

);
const regularBookingService = getRegularBookingService();
await regularBookingService.createBooking({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent UTC timezone fallback can reschedule a booking to the wrong absolute time

timeZone: event.timeZone ?? "UTC",

If the incoming calendar event has no timeZone field (common for whole-day events or some CalDAV clients), the booking is rescheduled as if it were in UTC. For a user in e.g. America/New_York whose event starts at 10:00, this silently shifts the booking by several hours rather than failing loudly.

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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional Medium

language: "en" is hardcoded in the rescheduleBooking call to createBooking. The booking object already contains user locale information accessible via the organizer's profile. Using a hardcoded "en" means all reschedule notifications triggered via calendar sync will be sent in English regardless of the organizer's and attendees' configured locale, breaking the i18n experience.

Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert bash developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 176-176
Issue Type: functional-medium
Severity: medium

Issue Description:
`language: "en"` is hardcoded in the `rescheduleBooking` call to `createBooking`. The booking object already contains user locale information accessible via the organizer's profile. Using a hardcoded `"en"` means all reschedule notifications triggered via calendar sync will be sent in English regardless of the organizer's and attendees' configured locale, breaking the i18n experience.

Current Code:
            language: "en",

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow bash best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

language: "en",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional Medium

rescheduleBooking passes a hardcoded language: "en" to createBooking. The booking service uses this language to look up translations for confirmation emails sent to all attendees. Attendees of non-English bookings will receive reschedule notifications in English regardless of their locale preference. The user's locale is available on the booking object (if selected in the repository query) or can default to the event's attendee locale. At minimum, this should fall back to the booking owner's locale rather than always forcing English.

Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert bash developer with deep knowledge of security, performance, and best practices.

### Context

File: packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts
Lines: 177-177
Issue Type: functional-medium
Severity: medium

Issue Description:
`rescheduleBooking` passes a hardcoded `language: "en"` to `createBooking`. The booking service uses this language to look up translations for confirmation emails sent to all attendees. Attendees of non-English bookings will receive reschedule notifications in English regardless of their locale preference. The user's locale is available on the `booking` object (if selected in the repository query) or can default to the event's attendee locale. At minimum, this should fall back to the booking owner's locale rather than always forcing English.

Current Code:
          language: "en",

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow bash best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

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" },
});
}
}
}
Loading