Skip to content

fix(bookings): set booking to PENDING when calendar sync fails#28916

Open
pmartin-dev wants to merge 2 commits intocalcom:mainfrom
pmartin-dev:fix/calendar-sync-failure-handling
Open

fix(bookings): set booking to PENDING when calendar sync fails#28916
pmartin-dev wants to merge 2 commits intocalcom:mainfrom
pmartin-dev:fix/calendar-sync-failure-handling

Conversation

@pmartin-dev
Copy link
Copy Markdown

What does this PR do?

When external calendar event creation fails (Google Calendar, Outlook, etc.), bookings were silently saved as ACCEPTED with no corresponding calendar event — creating "phantom bookings". This fix downgrades the booking to PENDING when all calendar integrations fail, and prevents misleading confirmation emails and webhook payloads.

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

No special environment variables needed. Run the unit tests:

TZ=UTC npx vitest run packages/features/bookings/lib/service/RegularBookingService.test.ts

3 test cases:

  1. Calendar sync fails → booking is PENDING
  2. Calendar sync fails (no video integration) → booking is PENDING
  3. Calendar sync succeeds → booking is ACCEPTED

To reproduce the original bug manually:

  1. Connect a Google Calendar
  2. Create a booking while the calendar API is unavailable (e.g. revoke credentials)
  3. Before fix: booking is ACCEPTED, no calendar event created
  4. After fix: booking is PENDING

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have checked that my changes generate no new warnings
  • My PR is small and focused (24 insertions, 3 deletions in 1 file + 1 new test file)

@github-actions
Copy link
Copy Markdown
Contributor

Welcome to Cal.diy, @pmartin-dev! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

@github-actions github-actions bot added the 🐛 bug Something isn't working label Apr 16, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

Adds a Vitest test suite for RegularBookingService covering calendar sync success and failure scenarios, and updates RegularBookingService to derive calendarResults, detect when all calendar integrations fail, persist bookings with BookingStatus.PENDING in that case, suppress confirmation emails/SMS when calendar events all fail, emit an error log with failing calendar results, and set webhook payload/status and other acceptance-side effects only when the booking remains ACCEPTED.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately and concisely describes the main change: setting bookings to PENDING when calendar sync fails, which is the primary objective of the changeset.
Description check ✅ Passed The description clearly explains the issue (phantom bookings when calendar sync fails), the solution (downgrade to PENDING), and provides comprehensive testing instructions and checklist items.
Linked Issues check ✅ Passed The PR successfully implements the recommended fix from #28397: saves bookings as PENDING on calendar creation failure, prevents confirmation emails/webhooks when sync fails, and includes test coverage for the three key scenarios.
Out of Scope Changes check ✅ Passed All changes are within scope: RegularBookingService.ts implements PENDING status logic and gates downstream side effects, and the new test file verifies calendar sync failure handling as required by #28397.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/features/bookings/lib/service/RegularBookingService.ts`:
- Around line 2082-2093: The code sets booking.status = BookingStatus.PENDING
when allCalendarEventsFailed but execution continues down the
isConfirmedByDefault branch, causing ACCEPTED-only side effects to still run;
update the flow in RegularBookingService so that after detecting
allCalendarEventsFailed you either (a) switch to the pending/requested path
(e.g., return or jump to the pending handler) or (b) add guards on downstream
side effects (the BOOKING_CREATED emit, BOOKING_REQUESTED queueing, and no-show
scheduling) to check booking.status === BookingStatus.ACCEPTED before executing;
specifically adjust the block that handles allCalendarEventsFailed (uses
booking, allCalendarEventsFailed, BookingStatus.PENDING) and protect the later
side-effect code paths that reference BOOKING_CREATED, BOOKING_REQUESTED, and
no-show scheduling to only run for BookingStatus.ACCEPTED.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 240ba73d-edbd-4309-9713-55de30d2cf6e

📥 Commits

Reviewing files that changed from the base of the PR and between 00693bb and 60ddfeb.

📒 Files selected for processing (2)
  • packages/features/bookings/lib/service/RegularBookingService.test.ts
  • packages/features/bookings/lib/service/RegularBookingService.ts

Comment thread packages/features/bookings/lib/service/RegularBookingService.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/features/bookings/lib/service/RegularBookingService.ts`:
- Around line 2082-2086: The current flow persists bookings as ACCEPTED before
EventManager.create() runs, causing a transient ACCEPTED state; change the
createBooking flow in RegularBookingService (the code that calls
EventManager.create() and uses deps.prismaClient.booking.update on booking) to
initially persist the booking record with status BookingStatus.PENDING, then
call EventManager.create(); after at least one calendar write succeeds,
explicitly update that booking row to BookingStatus.ACCEPTED (via
deps.prismaClient.booking.update), and only use the compensating rollback to
PENDING if all calendar writes fail. Locate the logic around createBooking(),
the booking variable, EventManager.create(), and the update call to
BookingStatus.PENDING/ACCEPTED and switch the order of operations so promotion
to ACCEPTED happens after successful calendar writes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c533875b-a0f4-4d88-87cc-1b690927c31d

📥 Commits

Reviewing files that changed from the base of the PR and between 60ddfeb and c41d711.

📒 Files selected for processing (1)
  • packages/features/bookings/lib/service/RegularBookingService.ts

Comment on lines +2082 to +2086
if (allCalendarEventsFailed) {
await deps.prismaClient.booking.update({
where: { id: booking.id },
data: { status: BookingStatus.PENDING },
});
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 16, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This still leaves a transient ACCEPTED booking in the DB.

By the time this runs, createBooking() has already persisted the confirmed-by-default booking earlier in the flow, so this is only a compensating update after EventManager.create() returns. Anything observing the row in that gap can still see the phantom ACCEPTED state the issue is trying to remove. Consider persisting these bookings as PENDING first and only promoting them to ACCEPTED after at least one calendar write succeeds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/bookings/lib/service/RegularBookingService.ts` around lines
2082 - 2086, The current flow persists bookings as ACCEPTED before
EventManager.create() runs, causing a transient ACCEPTED state; change the
createBooking flow in RegularBookingService (the code that calls
EventManager.create() and uses deps.prismaClient.booking.update on booking) to
initially persist the booking record with status BookingStatus.PENDING, then
call EventManager.create(); after at least one calendar write succeeds,
explicitly update that booking row to BookingStatus.ACCEPTED (via
deps.prismaClient.booking.update), and only use the compensating rollback to
PENDING if all calendar writes fail. Locate the logic around createBooking(),
the booking variable, EventManager.create(), and the update call to
BookingStatus.PENDING/ACCEPTED and switch the order of operations so promotion
to ACCEPTED happens after successful calendar writes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Acknowledged => the transient ACCEPTED window is a known trade-off. Persisting as PENDING first would require changing createBooking() which conditions all downstream side effects (notifications, payments, workflows). That's a much larger refactor with higher regression risk. The compensating update runs before any side effect fires, so no consumer observes the phantom state in practice.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@pmartin-dev, that's a fair and well-reasoned trade-off. Looking at the code flow confirms your claim: fireBookingEvents, the webhook trigger, scheduleNoShowTriggers, and the email dispatch are all gated on booking.status === BookingStatus.ACCEPTED or !allCalendarEventsFailed, and they all execute after the compensating booking.status = BookingStatus.PENDING mutation. So within this handler, no side effects can observe the phantom ACCEPTED state.

The only residual exposure is external DB readers (e.g., a separate process or replica polling the booking table) that happen to query between createBooking() completing and eventManager.create() + the compensating update finishing. That window is genuinely narrow and, for the scope of this fix vs. the regression risk of refactoring createBooking(), the trade-off is defensible.

Happy to close this out.


✏️ Learnings added
Learnt from: pmartin-dev
Repo: calcom/cal.diy PR: 28916
File: packages/features/bookings/lib/service/RegularBookingService.ts:2082-2086
Timestamp: 2026-04-16T13:00:11.708Z
Learning: In `packages/features/bookings/lib/service/RegularBookingService.ts`, when all calendar integrations fail (`allCalendarEventsFailed`), the booking is compensated to `BookingStatus.PENDING` via a `prismaClient.booking.update` call immediately after `eventManager.create()` returns. This is intentionally a compensating-update pattern (not an optimistic-PENDING-then-promote pattern), because `createBooking()` conditions downstream side effects on the initial status; refactoring that is considered too high-regression-risk. All side effects (emails, webhooks, no-show triggers) are gated on `booking.status === ACCEPTED` and fire after the in-memory mutation, so no consumer within the handler observes the transient `ACCEPTED` state. The only residual risk is external DB readers during the narrow window between `createBooking()` and the compensating update — accepted as a known trade-off.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug Something isn't working size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Booking Saved Without Calendar Event on External Sync Failure – Missing Atomicity / Rollback

1 participant