Skip to content

Conversation

weknowyourgame
Copy link
Contributor

@weknowyourgame weknowyourgame commented Jun 23, 2025

What does this PR do?

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

Summary by cubic

Added support for tracking expired reservations and sending a RESERVATION_EXPIRED webhook when a reservation expires.

  • New Features
    • Added a cron job to detect and process expired reservations.
    • Introduced the RESERVATION_EXPIRED webhook event, including UI, API, and documentation updates.
    • Added tests for the new webhook trigger and cron job.

Copy link

vercel bot commented Jun 23, 2025

@weknowyourgame is attempting to deploy a commit to the cal Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions bot added the ❗️ migrations contains migration files label Jun 23, 2025
@weknowyourgame weknowyourgame marked this pull request as ready for review June 23, 2025 19:39
@weknowyourgame weknowyourgame requested review from a team as code owners June 23, 2025 19:39
@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label Jun 23, 2025
@graphite-app graphite-app bot requested a review from a team June 23, 2025 19:40
Copy link

graphite-app bot commented Jun 23, 2025

Graphite Automations

"Add consumer team as reviewer" took an action on this PR • (06/23/25)

1 reviewer was added to this PR based on Keith Williams's automation.

"Add community label" took an action on this PR • (06/23/25)

1 label was added to this PR based on Keith Williams's automation.

@dosubot dosubot bot added the ✨ feature New feature or request label Jun 23, 2025
@github-actions github-actions bot added api area: API, enterprise API, access token, OAuth platform Anything related to our platform plan labels Jun 23, 2025
@kart1ka
Copy link
Contributor

kart1ka commented Jun 24, 2025

Hey @weknowyourgame . Thanks for the PR. Could you please add a loom video showing the feature?

@weknowyourgame
Copy link
Contributor Author

weknowyourgame commented Jun 24, 2025

@kart1ka

loom_1080p.1.mp4

Please let me know if this works or if any changes are needed

@anikdhabal anikdhabal added the Low priority Created by Linear-GitHub Sync label Jul 4, 2025
@weknowyourgame weknowyourgame requested a review from a team July 4, 2025 21:23
Copy link
Contributor

@kart1ka kart1ka left a comment

Choose a reason for hiding this comment

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

Left comments.

Comment on lines 68 to 89
const webhookData = {
eventType: "RESERVATION_EXPIRED",
reservationUid: reservation.uid,
eventTypeId: reservation.eventTypeId,
eventTypeTitle: eventType?.title,
slotStart: reservation.slotUtcStartDate.toISOString(),
slotEnd: reservation.slotUtcEndDate.toISOString(),
userId: reservation.userId,
userName: user?.name,
userEmail: user?.email,
expiredAt: reservation.releaseAt.toISOString(),
createdAt: new Date().toISOString(),
};

// Send webhooks
const promises = webhooks.map((webhook) =>
sendGenericWebhookPayload({
secretKey: webhook.secret,
triggerEvent: WebhookTriggerEvents.RESERVATION_EXPIRED,
createdAt: new Date().toISOString(),
webhook,
data: webhookData,
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like there are two createdAt fields being sent here. Let’s remove the one from the payload. It’s a bit confusing and makes it seem like it’s the booking’s creation time instead of the webhook trigger time.

Comment on lines 48 to 65
const webhooks = await prisma.webhook.findMany({
where: {
active: true,
eventTriggers: { has: WebhookTriggerEvents.RESERVATION_EXPIRED },
OR: [
// Event type specific webhooks
{ eventTypeId: reservation.eventTypeId },
// User specific webhooks
{ userId: eventType?.userId },
// Team specific webhooks
{ teamId: eventType?.teamId },
// Global webhooks (no specific eventTypeId, userId, or teamId)
{
AND: [{ eventTypeId: null }, { userId: null }, { teamId: null }],
},
],
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not take into account org wide webhooks. Org wide webhooks are not working for Reservation expired trigger.

Comment on lines 48 to 65
const webhooks = await prisma.webhook.findMany({
where: {
active: true,
eventTriggers: { has: WebhookTriggerEvents.RESERVATION_EXPIRED },
OR: [
// Event type specific webhooks
{ eventTypeId: reservation.eventTypeId },
// User specific webhooks
{ userId: eventType?.userId },
// Team specific webhooks
{ teamId: eventType?.teamId },
// Global webhooks (no specific eventTypeId, userId, or teamId)
{
AND: [{ eventTypeId: null }, { userId: null }, { teamId: null }],
},
],
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not needed. There is no such thing as global webhooks. cc @anikdhabal

@kart1ka kart1ka marked this pull request as draft July 8, 2025 05:31
@weknowyourgame weknowyourgame marked this pull request as ready for review July 9, 2025 09:03
@weknowyourgame weknowyourgame requested a review from kart1ka July 9, 2025 09:03
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

cubic found 5 issues across 14 files. Review them in cubic.dev

React with 👍 or 👎 to teach cubic. Tag @cubic-dev-ai to give specific feedback.

@dosubot dosubot bot added the webhooks area: webhooks, callback, webhook payload label Jul 9, 2025
@weknowyourgame
Copy link
Contributor Author

hey @kart1ka
I made all the changes as you requested
can you pls review it?

Copy link
Contributor

coderabbitai bot commented Jul 21, 2025

Walkthrough

This change set introduces a new "Reservation Expired" webhook trigger and supporting infrastructure for tracking and notifying on expired reservations. It adds a new cron-triggered GitHub Actions workflow that regularly calls an API endpoint to process expired reservations. The backend API route identifies reservations whose release time has passed, sends webhook notifications to configured endpoints, and deletes expired reservations, with robust error handling and logging. The Prisma schema and migrations are updated to support the new webhook event type. The webhook form UI and constants are extended to allow selection of the new trigger. Comprehensive unit and end-to-end tests validate the new logic, and documentation and localization are updated accordingly.

Estimated code review effort

4 (approximately one day)

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/app/api/cron/reservationExpiry/__tests__/cron.test.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-playwright".

(The package "eslint-plugin-playwright" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-playwright@latest --save-dev

The plugin "eslint-plugin-playwright" was referenced from the config file in ".eslintrc.js".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

apps/web/modules/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-playwright".

(The package "eslint-plugin-playwright" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-playwright@latest --save-dev

The plugin "eslint-plugin-playwright" was referenced from the config file in ".eslintrc.js".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

apps/web/app/api/cron/reservationExpiry/route.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-playwright".

(The package "eslint-plugin-playwright" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-playwright@latest --save-dev

The plugin "eslint-plugin-playwright" was referenced from the config file in ".eslintrc.js".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

  • 4 others
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
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: 3

♻️ Duplicate comments (1)
apps/web/app/api/cron/reservationExpiry/route.ts (1)

13-142: Remove duplicate createdAt field from webhook payload

The webhook payload is sending two createdAt fields - one in the payload data and another in the sendGenericWebhookPayload call. This is confusing as it suggests the booking's creation time rather than the webhook trigger time.

         if (webhooks.length > 0) {
           const webhookData = {
             eventType: "RESERVATION_EXPIRED",
             reservationUid: reservation.uid,
             eventTypeId: reservation.eventTypeId,
             eventTypeTitle: eventType?.title,
             slotStart: reservation.slotUtcStartDate.toISOString(),
             slotEnd: reservation.slotUtcEndDate.toISOString(),
             userId: reservation.userId,
             userName: user?.name,
             userEmail: user?.email,
             expiredAt: reservation.releaseAt.toISOString(),
+            createdAt: reservation.slotUtcStartDate.toISOString(),
           };
🧹 Nitpick comments (2)
packages/prisma/migrations/20250623105707_add_reservation_expired_webhook_trigger/migration.sql (1)

1-2: Make the enum alteration idempotent

If this migration is ever re-applied (for example on a restored database snapshot) it will fail because the value already exists. Adding the IF NOT EXISTS guard makes the change safe while remaining Postgres-version-compatible.

-ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'RESERVATION_EXPIRED';
+ALTER TYPE "WebhookTriggerEvents" ADD VALUE IF NOT EXISTS 'RESERVATION_EXPIRED';
apps/web/public/static/locales/en/common.json (1)

3381-3381: Key added in the right spot – consider propagating to all locales

"reservation_expired" is correctly inserted above the sentinel line, JSON syntax remains valid.
Follow-up: add this key to every supported locale file to avoid missing-string fallbacks at runtime.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb4260c and e5074c5.

📒 Files selected for processing (12)
  • .github/workflows/cron-reservationExpiry.yml (1 hunks)
  • apps/web/app/api/cron/reservationExpiry/__tests__/cron.test.ts (1 hunks)
  • apps/web/app/api/cron/reservationExpiry/route.ts (1 hunks)
  • apps/web/modules/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view.tsx (1 hunks)
  • apps/web/playwright/webhook.e2e.ts (1 hunks)
  • apps/web/public/static/locales/en/common.json (1 hunks)
  • docs/developing/guides/automation/webhooks.mdx (2 hunks)
  • packages/features/webhooks/components/WebhookForm.tsx (1 hunks)
  • packages/features/webhooks/lib/constants.ts (1 hunks)
  • packages/features/webhooks/lib/sendPayload.ts (3 hunks)
  • packages/prisma/migrations/20250623105707_add_reservation_expired_webhook_trigger/migration.sql (1 hunks)
  • packages/prisma/schema.prisma (1 hunks)
🧰 Additional context used
🧠 Learnings (6)
apps/web/modules/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view.tsx (1)

Learnt from: vijayraghav-io
PR: #21072
File: packages/app-store/office365calendar/api/webhook.ts:120-123
Timestamp: 2025-07-18T17:57:16.370Z
Learning: The office365calendar webhook handler in packages/app-store/office365calendar/api/webhook.ts is specifically designed for Office365 calendar integration, not as a generic webhook handler. Therefore, it's safe to assume that fetchAvailabilityAndSetCache method will be implemented in the Office365CalendarService, making explicit validation checks unnecessary.

packages/features/webhooks/components/WebhookForm.tsx (1)

Learnt from: vijayraghav-io
PR: #21072
File: packages/app-store/office365calendar/api/webhook.ts:120-123
Timestamp: 2025-07-18T17:57:16.370Z
Learning: The office365calendar webhook handler in packages/app-store/office365calendar/api/webhook.ts is specifically designed for Office365 calendar integration, not as a generic webhook handler. Therefore, it's safe to assume that fetchAvailabilityAndSetCache method will be implemented in the Office365CalendarService, making explicit validation checks unnecessary.

packages/features/webhooks/lib/constants.ts (1)

Learnt from: vijayraghav-io
PR: #21072
File: packages/app-store/office365calendar/api/webhook.ts:120-123
Timestamp: 2025-07-18T17:57:16.370Z
Learning: The office365calendar webhook handler in packages/app-store/office365calendar/api/webhook.ts is specifically designed for Office365 calendar integration, not as a generic webhook handler. Therefore, it's safe to assume that fetchAvailabilityAndSetCache method will be implemented in the Office365CalendarService, making explicit validation checks unnecessary.

apps/web/app/api/cron/reservationExpiry/route.ts (3)

Learnt from: vijayraghav-io
PR: #21072
File: packages/app-store/office365calendar/api/webhook.ts:120-123
Timestamp: 2025-07-18T17:57:16.370Z
Learning: The office365calendar webhook handler in packages/app-store/office365calendar/api/webhook.ts is specifically designed for Office365 calendar integration, not as a generic webhook handler. Therefore, it's safe to assume that fetchAvailabilityAndSetCache method will be implemented in the Office365CalendarService, making explicit validation checks unnecessary.

Learnt from: eunjae-lee
PR: #22106
File: packages/lib/server/service/insightsRouting.ts:367-368
Timestamp: 2025-07-15T13:02:17.359Z
Learning: In the InsightsRoutingService (packages/lib/server/service/insightsRouting.ts), multi-select filter data is already validated by zod before reaching the buildFormFieldSqlCondition method, so null/undefined values are not present in filterValue.data arrays and don't need to be filtered out.

Learnt from: eunjae-lee
PR: #22106
File: packages/features/insights/components/FailedBookingsByField.tsx:65-71
Timestamp: 2025-07-15T12:59:34.341Z
Learning: In the FailedBookingsByField component (packages/features/insights/components/FailedBookingsByField.tsx), although routingFormId is typed as optional in useInsightsParameters, the system automatically enforces a routing form filter, so routingFormId is always present in practice. This means the data always contains only one entry, making the single-entry destructuring approach safe.

apps/web/public/static/locales/en/common.json (1)

Learnt from: bandhan-majumder
PR: #22359
File: packages/lib/server/locales/en/common.json:1336-1339
Timestamp: 2025-07-14T16:31:45.201Z
Learning: When making localization changes for new features, it's often safer to add new strings rather than modify existing ones to avoid breaking existing functionality that depends on the original strings. This approach allows for feature-specific customization while maintaining backward compatibility.

packages/features/webhooks/lib/sendPayload.ts (1)

Learnt from: vijayraghav-io
PR: #21072
File: packages/app-store/office365calendar/api/webhook.ts:120-123
Timestamp: 2025-07-18T17:57:16.370Z
Learning: The office365calendar webhook handler in packages/app-store/office365calendar/api/webhook.ts is specifically designed for Office365 calendar integration, not as a generic webhook handler. Therefore, it's safe to assume that fetchAvailabilityAndSetCache method will be implemented in the Office365CalendarService, making explicit validation checks unnecessary.

🪛 actionlint (1.7.7)
.github/workflows/cron-reservationExpiry.yml

8-8: scheduled job runs too frequently. it runs once per 60 seconds. the shortest interval is once every 5 minutes

(events)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Security Check
🔇 Additional comments (12)
packages/prisma/schema.prisma (1)

968-988: Additions look good – make sure the rest of the stack is regenerated

RESERVATION_EXPIRED is appended at the end of WebhookTriggerEvents, so the ordering rules for Postgres enums are respected.
Please ensure the following follow-ups are executed before merging:

  1. Run prisma generate (and any zod-prisma/kysely code-gen) so the new enum value is reflected in the generated types.
  2. Re-run the full test suite and type-check; any switch/union exhaustiveness relying on the enum will now need to handle the new member.

No code changes required here – just the usual regeneration & verification steps.

packages/features/webhooks/components/WebhookForm.tsx (1)

60-60: LGTM! Clean addition of new webhook trigger event.

The new RESERVATION_EXPIRED webhook trigger event is properly added to the core events list and follows the existing pattern.

apps/web/modules/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view.tsx (1)

72-72: LGTM! Consistent addition of RESERVATION_EXPIRED trigger.

The new webhook trigger event is properly added to the OAuth client webhook options, maintaining consistency with the core webhook form.

packages/features/webhooks/lib/constants.ts (1)

21-21: LGTM! Proper addition to webhook constants.

The RESERVATION_EXPIRED event is correctly added to the core webhook trigger events constant, maintaining consistency across the codebase.

docs/developing/guides/automation/webhooks.mdx (2)

37-37: LGTM! Documentation properly updated for new trigger.

The Reservation Expired trigger is correctly added to the list of available webhook triggers in the documentation.


186-186: LGTM! Complete documentation of trigger events.

The RESERVATION_EXPIRED event is properly added to the comprehensive list of trigger event names in the webhook variable documentation.

apps/web/playwright/webhook.e2e.ts (1)

880-893: LGTM! Well-structured E2E test for the new webhook trigger.

The test properly verifies that the "Reservation Expired" option is available in the webhook creation UI. The use of page.getByRole("option", { name: "Reservation Expired" }) follows E2E best practices by using semantic role-based locators rather than raw text selectors.

.github/workflows/cron-reservationExpiry.yml (1)

16-23: Secure implementation of cron API call

Good security practices:

  • API key is passed via header instead of query parameters
  • Proper use of GitHub secrets
  • Conditional execution when secrets are available
packages/features/webhooks/lib/sendPayload.ts (2)

78-91: Well-structured payload type for reservation expired events

The ReservationExpiredPayloadType includes all necessary fields for tracking expired reservations. The structure is consistent with other payload types in the system.


209-215: Correct implementation of type guards

The type guard functions are properly implemented:

  • isReservationExpiredPayload correctly identifies the payload type
  • isEventPayload properly excludes all non-event payload types
apps/web/app/api/cron/reservationExpiry/route.ts (2)

57-78: Org-wide webhook support correctly implemented

The webhook query properly handles organization-wide webhooks by:

  1. Getting the org ID from member or team
  2. Including org webhooks in the OR condition when orgId differs from teamId

144-163: Secure authentication implementation

Good security practice using header-based authentication instead of query parameters. This prevents API keys from being exposed in logs, browser history, or referrers.

Comment on lines +7 to +8
# Runs "At every minute." (see https://crontab.guru)
- cron: "* * * * *"
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider reducing cron frequency to avoid excessive resource usage

Running a cron job every minute can put unnecessary load on your infrastructure. Consider running it less frequently (e.g., every 5-15 minutes) unless there's a specific business requirement for minute-level precision.

-    # Runs "At every minute." (see https://crontab.guru)
-    - cron: "* * * * *"
+    # Runs "At every 5 minutes." (see https://crontab.guru)
+    - cron: "*/5 * * * *"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Runs "At every minute." (see https://crontab.guru)
- cron: "* * * * *"
# Runs "At every 5 minutes." (see https://crontab.guru)
- cron: "*/5 * * * *"
🧰 Tools
🪛 actionlint (1.7.7)

8-8: scheduled job runs too frequently. it runs once per 60 seconds. the shortest interval is once every 5 minutes

(events)

🤖 Prompt for AI Agents
In .github/workflows/cron-reservationExpiry.yml at lines 7 to 8, the cron job is
scheduled to run every minute, which may cause excessive resource usage. Modify
the cron expression to run less frequently, such as every 5 or 15 minutes, by
changing the schedule from "* * * * *" to "*/5 * * * *" or "*/15 * * * *"
depending on the acceptable frequency for the task.

Comment on lines +14 to +83
async function handleReservationExpiry() {
const currentTimeInUtc = dayjs.utc().toDate();
let notificationsSent = 0;
let expiredReservationsProcessed = 0;

try {
// Find expired reservations that need to be processed
const expiredReservations = await prismock.selectedSlots.findMany({
where: {
releaseAt: { lt: currentTimeInUtc },
},
});

for (const reservation of expiredReservations) {
try {
// Get related data for webhook payload
const eventType = await prismock.eventType.findUnique({
where: { id: reservation.eventTypeId },
select: {
id: true,
title: true,
userId: true,
teamId: true,
},
});

// Get webhooks for this reservation
const webhooks = await prismock.webhook.findMany({
where: {
active: true,
eventTriggers: { has: WebhookTriggerEvents.RESERVATION_EXPIRED },
OR: [
{ eventTypeId: reservation.eventTypeId },
{ userId: eventType?.userId },
{ teamId: eventType?.teamId },
{
AND: [{ eventTypeId: null }, { userId: null }, { teamId: null }],
},
],
},
});

if (webhooks.length > 0) {
notificationsSent += webhooks.length;
}

// Clean up the expired reservation
await prismock.selectedSlots.delete({
where: { id: reservation.id },
});

expiredReservationsProcessed++;
} catch (error) {
console.error(`Error processing expired reservation ${reservation.uid}:`, error);
// Continue processing other reservations even if one fails
}
}

return {
notificationsSent,
expiredReservationsProcessed,
};
} catch (error) {
return {
error: "Internal server error",
notificationsSent,
expiredReservationsProcessed,
};
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Test handler implementation differs from production code

The test's handleReservationExpiry function is missing several features from the production implementation:

  1. Does not fetch user data (which is needed for webhook payload)
  2. Does not use sendGenericWebhookPayload
  3. Has different webhook query logic (includes global webhooks which don't exist)

This could lead to tests passing while production code fails.


🏁 Script executed:

#!/bin/bash
# Compare the test implementation with the production implementation
echo "=== Production implementation ==="
sed -n '13,142p' apps/web/app/api/cron/reservationExpiry/route.ts | head -20

echo -e "\n=== Test implementation ==="
sed -n '14,83p' apps/web/app/api/cron/reservationExpiry/__tests__/cron.test.ts | head -20

Length of output: 1535


🏁 Script executed:

#!/bin/bash
echo "=== Production handleReservationExpiry ==="
sed -n '1,200p' apps/web/app/api/cron/reservationExpiry/route.ts

echo -e "\n=== Test file ==="
sed -n '1,200p' apps/web/app/api/cron/reservationExpiry/__tests__/cron.test.ts

Length of output: 11587


Align test implementation with the production handler

The test file is redefining handleReservationExpiry instead of importing and exercising the real implementation in route.ts. As a result, key logic—user lookup, org lookup, webhook filtering, and calls to sendGenericWebhookPayload—is never tested against the actual code.

Please update the tests to:

  • Import handleReservationExpiry from apps/web/app/api/cron/reservationExpiry/route.ts rather than redefining it
  • Mock and assert calls to sendGenericWebhookPayload (instead of just counting webhooks)
  • Include mocks for prisma.user.findUnique and getOrgIdFromMemberOrTeamId to exercise organization-level webhooks
  • Align the webhook.findMany expectations with the production OR conditions (drop the global fallback that tests currently include)
  • Verify that errors flow through the logger (not console.error) and that retries/cleanup behave as in production

These changes will ensure your tests accurately reflect and validate the real cron handler logic.

🤖 Prompt for AI Agents
In apps/web/app/api/cron/reservationExpiry/__tests__/cron.test.ts from lines 14
to 83, the test redefines handleReservationExpiry instead of importing it from
the production code in route.ts. To fix this, import handleReservationExpiry
from apps/web/app/api/cron/reservationExpiry/route.ts, mock and assert calls to
sendGenericWebhookPayload rather than just counting webhooks, add mocks for
prisma.user.findUnique and getOrgIdFromMemberOrTeamId to cover
organization-level webhook logic, update webhook.findMany mocks to match the
production OR conditions without the global fallback, and replace console.error
assertions with logger error checks to reflect production error handling and
retry/cleanup behavior accurately.

Comment on lines +137 to +272
expect(result.expiredReservationsProcessed).toBe(1);
expect(result.notificationsSent).toBe(1);

// Check that expired reservation was deleted
const remainingReservations = await prismock.selectedSlots.findMany();
expect(remainingReservations).toHaveLength(0);
});

it("should not process future reservations", async () => {
const currentTime = dayjs.utc();
const futureTime = currentTime.add(1, "hour").toDate();

// Create test data with future expiry
await createUser({ id: 101 });
await createEventType({ id: 1, userId: 101 });
await createSelectedSlot({
id: 1,
eventTypeId: 1,
userId: 101,
uid: "future-reservation",
releaseAt: futureTime,
});
await createWebhook({
id: "webhook-1",
userId: 101,
eventTypeId: 1,
});

const result = await handleReservationExpiry();

expect(result.expiredReservationsProcessed).toBe(0);
expect(result.notificationsSent).toBe(0);

// Check that future reservation was not deleted
const remainingReservations = await prismock.selectedSlots.findMany();
expect(remainingReservations).toHaveLength(1);
});

it("should handle team webhooks for expired reservations", async () => {
const currentTime = dayjs.utc();
const expiredTime = currentTime.subtract(1, "hour").toDate();

// Create test data with team webhook
await createUser({ id: 101 });
await createEventType({ id: 1, userId: 101, teamId: 1 });
await createSelectedSlot({
id: 1,
eventTypeId: 1,
userId: 101,
uid: "team-expired-reservation",
releaseAt: expiredTime,
});
await createWebhook({
id: "team-webhook-1",
teamId: 1,
});

const result = await handleReservationExpiry();

expect(result.expiredReservationsProcessed).toBe(1);
expect(result.notificationsSent).toBe(1);
});

it("should process multiple expired reservations", async () => {
const currentTime = dayjs.utc();
const expiredTime = currentTime.subtract(1, "hour").toDate();

// Create test data with multiple expired reservations
await createUser({ id: 101 });
await createEventType({ id: 1, userId: 101 });
await createSelectedSlot({
id: 1,
eventTypeId: 1,
userId: 101,
uid: "expired-reservation-1",
releaseAt: expiredTime,
});
await createSelectedSlot({
id: 2,
eventTypeId: 1,
userId: 101,
uid: "expired-reservation-2",
releaseAt: expiredTime,
});
await createWebhook({
id: "webhook-1",
userId: 101,
eventTypeId: 1,
});

const result = await handleReservationExpiry();

expect(result.expiredReservationsProcessed).toBe(2);
expect(result.notificationsSent).toBe(2);

// Check that all expired reservations were deleted
const remainingReservations = await prismock.selectedSlots.findMany();
expect(remainingReservations).toHaveLength(0);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider importing and testing the actual production function

Instead of duplicating the implementation in tests, import the actual handleReservationExpiry function from the route file. This ensures tests accurately reflect production behavior.

-// Handler function for processing expired reservations
-async function handleReservationExpiry() {
-  // ... duplicated implementation
-}
+import { handleReservationExpiry } from "../route";

+// Mock prisma to use prismock
+vi.mock("@calcom/prisma", () => ({
+  default: prismock,
+}));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe("Reservation Expiry Cron Job", () => {
beforeEach(() => {
prismock.selectedSlots.deleteMany();
prismock.webhook.deleteMany();
prismock.eventType.deleteMany();
prismock.user.deleteMany();
});
it("should return zero counts when no expired reservations exist", async () => {
const result = await handleReservationExpiry();
expect(result.expiredReservationsProcessed).toBe(0);
expect(result.notificationsSent).toBe(0);
});
it("should process expired reservations and send webhooks", async () => {
const currentTime = dayjs.utc();
const expiredTime = currentTime.subtract(1, "hour").toDate();
// Create test data
await createUser({ id: 101 });
await createEventType({ id: 1, userId: 101 });
await createSelectedSlot({
id: 1,
eventTypeId: 1,
userId: 101,
uid: "expired-reservation-1",
releaseAt: expiredTime,
});
await createWebhook({
id: "webhook-1",
userId: 101,
eventTypeId: 1,
});
const result = await handleReservationExpiry();
expect(result.expiredReservationsProcessed).toBe(1);
expect(result.notificationsSent).toBe(1);
// Check that expired reservation was deleted
const remainingReservations = await prismock.selectedSlots.findMany();
expect(remainingReservations).toHaveLength(0);
});
it("should not process future reservations", async () => {
const currentTime = dayjs.utc();
const futureTime = currentTime.add(1, "hour").toDate();
// Create test data with future expiry
await createUser({ id: 101 });
await createEventType({ id: 1, userId: 101 });
await createSelectedSlot({
id: 1,
eventTypeId: 1,
userId: 101,
uid: "future-reservation",
releaseAt: futureTime,
});
await createWebhook({
id: "webhook-1",
userId: 101,
eventTypeId: 1,
});
const result = await handleReservationExpiry();
expect(result.expiredReservationsProcessed).toBe(0);
expect(result.notificationsSent).toBe(0);
// Check that future reservation was not deleted
const remainingReservations = await prismock.selectedSlots.findMany();
expect(remainingReservations).toHaveLength(1);
});
it("should handle team webhooks for expired reservations", async () => {
const currentTime = dayjs.utc();
const expiredTime = currentTime.subtract(1, "hour").toDate();
// Create test data with team webhook
await createUser({ id: 101 });
await createEventType({ id: 1, userId: 101, teamId: 1 });
await createSelectedSlot({
id: 1,
eventTypeId: 1,
userId: 101,
uid: "team-expired-reservation",
releaseAt: expiredTime,
});
await createWebhook({
id: "team-webhook-1",
teamId: 1,
});
const result = await handleReservationExpiry();
expect(result.expiredReservationsProcessed).toBe(1);
expect(result.notificationsSent).toBe(1);
});
it("should process multiple expired reservations", async () => {
const currentTime = dayjs.utc();
const expiredTime = currentTime.subtract(1, "hour").toDate();
// Create test data with multiple expired reservations
await createUser({ id: 101 });
await createEventType({ id: 1, userId: 101 });
await createSelectedSlot({
id: 1,
eventTypeId: 1,
userId: 101,
uid: "expired-reservation-1",
releaseAt: expiredTime,
});
await createSelectedSlot({
id: 2,
eventTypeId: 1,
userId: 101,
uid: "expired-reservation-2",
releaseAt: expiredTime,
});
await createWebhook({
id: "webhook-1",
userId: 101,
eventTypeId: 1,
});
const result = await handleReservationExpiry();
expect(result.expiredReservationsProcessed).toBe(2);
expect(result.notificationsSent).toBe(2);
// Check that all expired reservations were deleted
const remainingReservations = await prismock.selectedSlots.findMany();
expect(remainingReservations).toHaveLength(0);
});
});
// Remove the locally defined handler and instead import the real one
import { describe, it, expect, beforeEach, vi } from "vitest";
import dayjs from "dayjs";
import { createUser, createEventType, createSelectedSlot, createWebhook } from "../helpers";
import prismock from "@calcom/prisma";
import { handleReservationExpiry } from "../route";
// Ensure the production code’s prisma import is redirected to our in-memory mock
vi.mock("@calcom/prisma", () => ({
default: prismock,
}));
describe("Reservation Expiry Cron Job", () => {
beforeEach(() => {
prismock.selectedSlots.deleteMany();
prismock.webhook.deleteMany();
prismock.eventType.deleteMany();
prismock.user.deleteMany();
});
it("should return zero counts when no expired reservations exist", async () => {
const result = await handleReservationExpiry();
expect(result.expiredReservationsProcessed).toBe(0);
expect(result.notificationsSent).toBe(0);
});
it("should process expired reservations and send webhooks", async () => {
const currentTime = dayjs.utc();
const expiredTime = currentTime.subtract(1, "hour").toDate();
// Create test data
await createUser({ id: 101 });
await createEventType({ id: 1, userId: 101 });
await createSelectedSlot({
id: 1,
eventTypeId: 1,
userId: 101,
uid: "expired-reservation-1",
releaseAt: expiredTime,
});
await createWebhook({
id: "webhook-1",
userId: 101,
eventTypeId: 1,
});
const result = await handleReservationExpiry();
expect(result.expiredReservationsProcessed).toBe(1);
expect(result.notificationsSent).toBe(1);
// Check that expired reservation was deleted
const remainingReservations = await prismock.selectedSlots.findMany();
expect(remainingReservations).toHaveLength(0);
});
// …the rest of the test cases remain unchanged…
});
🤖 Prompt for AI Agents
In apps/web/app/api/cron/reservationExpiry/__tests__/cron.test.ts around lines
137 to 272, the tests currently duplicate the implementation logic instead of
using the real production function. To fix this, import the actual
handleReservationExpiry function from the production route file at the top of
the test file and replace any duplicated or mock implementations with calls to
this imported function. This will ensure the tests run against the real
production code and better validate its behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api area: API, enterprise API, access token, OAuth community Created by Linear-GitHub Sync ✨ feature New feature or request Low priority Created by Linear-GitHub Sync ❗️ migrations contains migration files platform Anything related to our platform plan webhooks area: webhooks, callback, webhook payload
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feature: expired reservation tracking
3 participants