Skip to content

feat: add org-level analytics for cancellation and reschedule reasons#28563

Draft
PeerRich wants to merge 3 commits intomainfrom
devin/pri-423-1774364831
Draft

feat: add org-level analytics for cancellation and reschedule reasons#28563
PeerRich wants to merge 3 commits intomainfrom
devin/pri-423-1774364831

Conversation

@PeerRich
Copy link
Copy Markdown
Member

@PeerRich PeerRich commented Mar 24, 2026

What does this PR do?

Adds org-level analytics for cancellation and reschedule reasons to the Insights page, so organization admins can track these metrics across their entire org — not just per-host.

  • Fixes PRI-423

Changes

Database layer:

  • Adds cancellationReason column to BookingDenormalized table
  • Drops and recreates BookingTimeStatusDenormalized view to expose the new column (PostgreSQL's CREATE OR REPLACE VIEW cannot add columns before existing ones)
  • Migration backfills existing data from Booking table and updates the trigger function

Service layer:

  • Adds getCancellationReasonStats() and getRescheduleReasonStats() to InsightsBookingBaseService
  • Both query cancellationReason from the denormalized view, distinguished by timeStatus ('cancelled' vs 'rescheduled')
  • Uses NULLIF(BTRIM("cancellationReason"), '') to filter out NULL, empty, and whitespace-only reasons

API layer:

  • Adds cancellationReasons and rescheduleReasons tRPC endpoints behind userBelongsToTeamProcedure

UI layer:

  • New CancellationReasonsTable and RescheduleReasonsTable components using existing ChartCard/ChartCardItem pattern
  • Added to the insights page grid alongside existing analytics cards

Tests:

  • Unit tests for both service methods covering happy path, empty results, and query shape validation

Important review notes

  1. Both methods read cancellationReason — Cal.com stores the reschedule reason in the same cancellationReason field on the Booking model. The getRescheduleReasonStats method distinguishes reschedules by filtering "timeStatus" = 'rescheduled'. Please verify this is the intended data source.

  2. Migration drops and recreates the view — The migration uses DROP VIEW IF EXISTS + CREATE VIEW (not CREATE OR REPLACE VIEW, which can't add columns before existing ones). Please verify the view body matches current production with only the cancellationReason addition. Similarly, the trigger function is replaced via CREATE OR REPLACE FUNCTION — verify its body matches production.

  3. bookingDataSchema updated — Added cancellationReason: z.string().nullable() to the strict zod schema. If any existing raw queries return data validated against this schema, they would need to include cancellationReason in their SELECT or the strict parse will fail.

  4. No visual demo — These components were not tested in a running app. They follow the existing ChartCard pattern used by MostCancelledBookingsTables, HighestNoShowHostTable, etc.

Human review checklist

  • Verify trigger function body in the migration matches current production (with only cancellationReason addition)
  • Verify the recreated view definition matches the original
  • Confirm reschedule reasons are stored in the cancellationReason field on Booking
  • Check that adding cancellationReason to bookingDataSchema (.strict()) doesn't break existing query consumers

Visual Demo (For contributors especially)

N/A — no visual demo available. Components follow the established ChartCard/ChartCardItem pattern.

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. N/A — no docs changes needed.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  1. Apply the migration against a database with existing bookings that have cancellationReason values
  2. Navigate to the Insights page as an org admin
  3. Verify the "Cancellation reasons" and "Reschedule reasons" cards appear in the grid
  4. Verify they show aggregated reason counts (top 10, ordered by frequency)
  5. Verify empty state message appears when no reasons exist
  6. Verify whitespace-only reasons are excluded from results
  7. Verify the cards respect org/team/user scope filters and date range filters

Test data needed: Bookings with cancellationReason set, some with status = 'cancelled' and some that were rescheduled.

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have checked if my changes generate no new warnings
  • New and existing unit tests pass locally with my changes

Link to Devin session: https://app.devin.ai/sessions/cca15943320f47ac9ed6b7993910b012
Requested by: @PeerRich

- Add cancellationReason field to BookingDenormalized model
- Update BookingTimeStatusDenormalized view to include cancellationReason
- Update trigger function to sync cancellationReason from Booking table
- Add getCancellationReasonStats() and getRescheduleReasonStats() service methods
- Add cancellationReasons and rescheduleReasons tRPC endpoints
- Create CancellationReasonsTable and RescheduleReasonsTable UI components
- Add components to insights page layout
- Add i18n translation strings
- Add unit tests for new service methods

Closes PRI-423

Co-Authored-By: peer@cal.com <peer@cal.com>
@linear
Copy link
Copy Markdown

linear bot commented Mar 24, 2026

@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions github-actions bot added the ❗️ migrations contains migration files label Mar 24, 2026
PostgreSQL cannot add new columns before existing ones with
CREATE OR REPLACE VIEW. Use DROP VIEW + CREATE VIEW instead.

Co-Authored-By: peer@cal.com <peer@cal.com>
Copy link
Copy Markdown
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.

1 issue found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/features/insights/services/InsightsBookingBaseService.ts">

<violation number="1" location="packages/features/insights/services/InsightsBookingBaseService.ts:1349">
P2: Whitespace-only cancellation reasons are not filtered out, so analytics can show blank reason rows.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 24, 2026

Devin AI is addressing Cubic AI's review feedback

New feedback has been sent to the existing Devin session.

View Devin Session


✅ Pushed commit c6a1162

Use NULLIF(BTRIM(...), '') to exclude whitespace-only reasons
from analytics, not just empty strings.

Identified by cubic.

Co-Authored-By: peer@cal.com <peer@cal.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

❗️ migrations contains migration files size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant