Skip to content

Comments

test: add comprehensive unit tests for no-show fee payment functions#23626

Merged
emrysal merged 48 commits intomainfrom
devin/unit-tests-no-show-fees-1757085131
Sep 5, 2025
Merged

test: add comprehensive unit tests for no-show fee payment functions#23626
emrysal merged 48 commits intomainfrom
devin/unit-tests-no-show-fees-1757085131

Conversation

@joeauyeung
Copy link
Contributor

What does this PR do?

This PR adds comprehensive unit tests for two critical no-show fee payment functions:

  • packages/lib/payment/handleNoShowFee.ts - Core function that processes no-show fee charges
  • packages/lib/payment/processNoShowFeeOnCancellation.ts - Function that determines when to charge no-show fees on cancellation

The tests provide extensive coverage including successful scenarios, error handling, and edge cases to ensure the no-show fee functionality works correctly across different payment scenarios and user permissions.

Link to Devin run: https://app.devin.ai/sessions/dd255a976d4041f7a1a59b6a9e5f6efe
Requested by: @joeauyeung

Test Coverage Added

handleNoShowFee.test.ts (14 tests)

  • ✅ Successful scenarios: individual users, team events, parent organization credential lookup
  • ✅ Error scenarios: missing user ID, team membership validation, missing credentials, payment app issues
  • ✅ Edge cases: missing event types, user details, attendee locales

processNoShowFeeOnCancellation.test.ts (15 tests)

  • ✅ Successful scenarios: fee processing when conditions are met
  • ✅ Skip scenarios: organizer cancellation, team admin/owner cancellation, payment conditions, time thresholds
  • ✅ Error scenarios: handleNoShowFee failures
  • ✅ Edge cases: complex metadata, missing event types

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.

How should this be tested?

Run the test suites to verify comprehensive coverage:

# Run individual test files
TZ=UTC yarn test packages/lib/payment/handleNoShowFee.test.ts
TZ=UTC yarn test packages/lib/payment/processNoShowFeeOnCancellation.test.ts

# Verify type checking passes
yarn type-check:ci --force

Expected Results:

  • All 29 tests should pass (14 + 15)
  • No TypeScript errors
  • Tests cover all major code paths and error scenarios

Important Review Points

⚠️ Critical areas for review:

  1. Mock Accuracy: Verify that mock objects (credentials, payments, repositories) accurately reflect real data structures and behavior
  2. Dynamic Import Mocking: Check that PaymentServiceMap mocking strategy aligns with actual app-store service loading
  3. Error Message Validation: Ensure error scenarios match what the actual functions would throw in production
  4. Team Permission Logic: Validate team admin/owner permission checking logic matches real membership validation

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
  • Tests use proper timezone handling with TZ=UTC
  • All mocks are properly reset between tests

devin-ai-integration bot and others added 30 commits September 4, 2025 16:38
- Add configurable time threshold (minutes/hours/days) for cancellation fees
- Show warnings during booking submission and cancellation
- Automatically charge fees when bookings cancelled within threshold
- Exempt organizer/admin cancellations from fees
- Extend existing no-show fee infrastructure

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
- Use Record<string, unknown> instead of any for metadata type
- Add proper type assertions for nested metadata properties
- Maintain type safety while avoiding ESLint no-explicit-any warning

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
- Add proper type guard for metadata JsonValue from Prisma
- Ensure metadata is object before casting to Record<string, unknown>
- Fixes TypeScript error on line 390 in handleCancelBooking.ts

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
@vercel
Copy link

vercel bot commented Sep 5, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal Ignored Ignored Sep 5, 2025 9:07pm
cal-eu Ignored Ignored Sep 5, 2025 9:07pm

Base automatically changed from devin/1757001996-no-show-cancellation-fees to main September 5, 2025 17:46
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We only need to getTranslations for the first attendee of the booking so we're removing the looped await

paymentOption: true,
appId: true,
success: true,
data: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Missing data needed when manually charging a card

booking: bookingToDelete,
teamId,
});
try {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wrapping these payment functions in a try/catch since the booking is already cancelled. If these fail we shouldn't throw an error to the user

@joeauyeung joeauyeung marked this pull request as ready for review September 5, 2025 19:29
@joeauyeung joeauyeung requested a review from a team as a code owner September 5, 2025 19:29
@graphite-app graphite-app bot requested a review from a team September 5, 2025 19:29
@dosubot dosubot bot added the automated-tests area: unit tests, e2e tests, playwright label Sep 5, 2025
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/lib/payment/handleNoShowFee.ts (1)

163-164: Fix ReferenceError: attendeesListPromises is undefined

This will throw at runtime. Use attendeesList[0].

-    await sendNoShowFeeChargedEmail(attendeesListPromises[0], evt, eventTypeMetdata);
+    await sendNoShowFeeChargedEmail(attendeesList[0], evt, eventTypeMetdata);
♻️ Duplicate comments (1)
packages/lib/server/repository/booking.ts (1)

988-989: LGTM: adding payment.data unblocks manual charge logic

This addresses the earlier “Missing data needed when manually charging a card” note.

🧹 Nitpick comments (12)
packages/lib/server/repository/booking.ts (1)

980-991: Confirm data safety and scope for payment.data

The payment.data blob can contain provider-returned PII/tokens. Please confirm this is only used server-side and never serialized to tRPC/API responses. If it ever crosses boundaries, redact it or project only required subkeys.

packages/features/bookings/lib/handleCancelBooking.ts (1)

458-466: Minor: unify error logging format

Elsewhere in this file we often use safeStringify; consider doing the same here for consistency and structured logs.

-      } catch (error) {
-        log.error(`Error processing payment refund for booking ${bookingToDelete.uid}:`, error);
+      } catch (error) {
+        log.error(
+          `Error processing payment refund for booking ${bookingToDelete.uid}:`,
+          safeStringify({ error })
+        );
       }
...
-      } catch (error) {
-        log.error(`Error processing no-show fee for booking ${bookingToDelete.uid}:`, error);
+      } catch (error) {
+        log.error(
+          `Error processing no-show fee for booking ${bookingToDelete.uid}:`,
+          safeStringify({ error })
+        );
       }

Also applies to: 468-476

packages/lib/payment/handleNoShowFee.ts (1)

65-66: Nit: variable name typo

eventTypeMetdata → eventTypeMetadata for readability.

-  const eventTypeMetdata = eventTypeMetaDataSchemaWithTypedApps.parse(booking.eventType?.metadata ?? {});
+  const eventTypeMetadata = eventTypeMetaDataSchemaWithTypedApps.parse(booking.eventType?.metadata ?? {});

And update its usage accordingly.

packages/lib/payment/processNoShowFeeOnCancellation.test.ts (2)

493-507: Test title contradicts expectations

The test expects skipping when HOLD is already successful, but the name says “should still be processed”. Rename for clarity.

-    it("should handle successful HOLD payment (should still be processed)", async () => {
+    it("should skip when HOLD payment is already successful", async () => {

24-44: Optional: add invalid metadata parse case

processNoShowFeeOnCancellation uses zod.parse; invalid metadata will throw before try/catch. Consider a test asserting it throws (and, optionally, a follow-up in handleCancelBooking to confirm it’s caught and logged).

packages/lib/payment/handleNoShowFee.test.ts (7)

59-67: Use vi.resetAllMocks() for stronger test isolation.

clearAllMocks doesn’t reset custom implementations (e.g., getTranslation). Prefer resetAllMocks to avoid leakage between tests.

Apply:

-    vi.clearAllMocks();
+    vi.resetAllMocks();

185-228: Tighten mock sequencing in org fallback test.

You set findPaymentCredentialByAppIdAndUserIdOrTeamId twice; the second resolve isn’t used on this path. Simplify to reduce noise.

Apply:

-      vi.mocked(CredentialRepository.findPaymentCredentialByAppIdAndUserIdOrTeamId)
-        .mockResolvedValueOnce(null)
-        .mockResolvedValueOnce(mockCredential);
+      vi.mocked(CredentialRepository.findPaymentCredentialByAppIdAndUserIdOrTeamId)
+        .mockResolvedValueOnce(null);

246-263: Assert no downstream calls when membership check fails.

Guarantee early-exit by asserting no credential lookup or charge attempt.

Apply:

       await expect(
         handleNoShowFee({
           booking: teamBooking,
           payment: mockPayment,
         })
       ).rejects.toThrow("User is not a member of the team");
+      expect(CredentialRepository.findPaymentCredentialByAppIdAndUserIdOrTeamId).not.toHaveBeenCalled();
+      expect(mockPaymentService.chargeCard).not.toHaveBeenCalled();

265-286: Assert no charge when credentials are missing.

Make sure we don’t attempt a payment without credentials.

Apply:

       await expect(
         handleNoShowFee({
           booking: bookingWithoutCredential,
           payment: mockPayment,
         })
       ).rejects.toThrow("No payment credential found");
+      expect(mockPaymentService.chargeCard).not.toHaveBeenCalled();

334-347: Assert email isn’t sent when processing fails.

If chargeCard returns null, ensure no email goes out.

Apply:

       await expect(
         handleNoShowFee({
           booking: mockBooking,
           payment: mockPayment,
         })
       ).rejects.toThrow("Payment processing failed");
+      expect(sendNoShowFeeChargedEmail).not.toHaveBeenCalled();

425-451: Differentiate organizer vs attendee locale fallback.

Set organizer to a non-en locale and assert both translation calls (organizer’s locale and attendee fallback en).

Apply:

-      const bookingWithAttendeesWithoutLocale = {
-        ...mockBooking,
-        attendees: [
+      const bookingWithAttendeesWithoutLocale = {
+        ...mockBooking,
+        user: { ...mockBooking.user, locale: "fr" },
+        attendees: [
           {
             name: "Jane Attendee",
             email: "attendee@example.com",
             timeZone: "UTC",
             locale: null,
           },
         ],
       };
@@
       expect(result).toEqual({ success: true, paymentId: "pay_123" });
-      expect(getTranslation).toHaveBeenCalledWith("en", "common");
+      expect(getTranslation).toHaveBeenCalledWith("fr", "common"); // organizer
+      expect(getTranslation).toHaveBeenCalledWith("en", "common"); // attendee fallback

129-146: Strengthen assertions in no-show fee test

  • Expect sendNoShowFeeChargedEmail to have been called once; verify it’s invoked with mockBooking.attendees[0].email and an event containing paymentInfo matching mockPayment.
  • Assert the dynamic PaymentService constructor is called with the resolved mockCredential.

Apply:

       expect(result).toEqual({ success: true, paymentId: "pay_123" });
       expect(mockPaymentService.chargeCard).toHaveBeenCalledWith(mockPayment, mockBooking.id);
-      expect(sendNoShowFeeChargedEmail).toHaveBeenCalled();
+      expect(sendNoShowFeeChargedEmail).toHaveBeenCalledTimes(1);
+      const [attendee, eventArg] = vi.mocked(sendNoShowFeeChargedEmail).mock.calls[0];
+      expect(attendee.email).toBe(mockBooking.attendees[0].email);
+      expect(eventArg.paymentInfo).toEqual({
+        amount: mockPayment.amount,
+        currency: mockPayment.currency,
+        paymentOption: mockPayment.paymentOption,
+      });
+      const module = await PaymentServiceMap.stripepayment;
+      expect(module.PaymentService).toHaveBeenCalledWith(mockCredential);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8282557 and 967ad12.

📒 Files selected for processing (5)
  • packages/features/bookings/lib/handleCancelBooking.ts (1 hunks)
  • packages/lib/payment/handleNoShowFee.test.ts (1 hunks)
  • packages/lib/payment/handleNoShowFee.ts (1 hunks)
  • packages/lib/payment/processNoShowFeeOnCancellation.test.ts (1 hunks)
  • packages/lib/server/repository/booking.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/bookings/lib/handleCancelBooking.ts
  • packages/lib/payment/processNoShowFeeOnCancellation.test.ts
  • packages/lib/payment/handleNoShowFee.test.ts
  • packages/lib/server/repository/booking.ts
  • packages/lib/payment/handleNoShowFee.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/bookings/lib/handleCancelBooking.ts
  • packages/lib/payment/processNoShowFeeOnCancellation.test.ts
  • packages/lib/payment/handleNoShowFee.test.ts
  • packages/lib/server/repository/booking.ts
  • packages/lib/payment/handleNoShowFee.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/bookings/lib/handleCancelBooking.ts
  • packages/lib/payment/processNoShowFeeOnCancellation.test.ts
  • packages/lib/payment/handleNoShowFee.test.ts
  • packages/lib/server/repository/booking.ts
  • packages/lib/payment/handleNoShowFee.ts
🧠 Learnings (2)
📚 Learning: 2025-08-21T12:28:42.018Z
Learnt from: alishaz-polymath
PR: calcom/cal.com#23247
File: packages/features/webhooks/lib/factory/WebhookPayloadFactory.ts:274-282
Timestamp: 2025-08-21T12:28:42.018Z
Learning: In BookingPaymentInitiatedDTO and other webhook DTOs in packages/features/webhooks/lib/dto/types.ts, the booking field is a restricted structure containing only specific fields (id, eventTypeId, userId) rather than the full database booking object, so there are no security or PII leakage concerns when passing the booking object to buildEventPayload.

Applied to files:

  • packages/lib/server/repository/booking.ts
📚 Learning: 2025-08-21T13:44:06.805Z
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.805Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.

Applied to files:

  • packages/lib/server/repository/booking.ts
🧬 Code graph analysis (3)
packages/features/bookings/lib/handleCancelBooking.ts (2)
apps/api/v2/src/lib/logger.bridge.ts (1)
  • error (77-79)
packages/lib/payment/processNoShowFeeOnCancellation.ts (1)
  • processNoShowFeeOnCancellation (9-77)
packages/lib/payment/processNoShowFeeOnCancellation.test.ts (3)
packages/lib/payment/shouldChargeNoShowCancellationFee.ts (1)
  • shouldChargeNoShowCancellationFee (5-58)
packages/lib/payment/handleNoShowFee.ts (1)
  • handleNoShowFee (19-175)
packages/lib/payment/processNoShowFeeOnCancellation.ts (1)
  • processNoShowFeeOnCancellation (9-77)
packages/lib/payment/handleNoShowFee.test.ts (4)
packages/app-store/payment.services.generated.ts (1)
  • PaymentServiceMap (5-12)
packages/lib/server/repository/credential.ts (1)
  • CredentialRepository (29-289)
packages/emails/email-manager.ts (1)
  • sendNoShowFeeChargedEmail (741-748)
packages/lib/payment/handleNoShowFee.ts (1)
  • handleNoShowFee (19-175)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (6)
packages/features/bookings/lib/handleCancelBooking.ts (2)

458-466: Don’t block cancellation on refund failures — good

Wrapping the refund in try/catch is the right tradeoff for UX.


468-476: Graceful no-show fee handling — good

Catching errors from no-show fee processing preserves the cancel flow as intended.

packages/lib/payment/handleNoShowFee.ts (1)

141-148: Verify dynamic import usage shape

If PaymentServiceMap values are functions returning promises, this should be awaited as paymentAppImportFn(). If they are already promises, awaiting the value is correct. Please confirm the generated shape to avoid a subtle runtime bug.

packages/lib/payment/processNoShowFeeOnCancellation.test.ts (1)

24-509: Strong, comprehensive coverage

Good spread of success, skip, payment-selection, threshold, error, and metadata edge cases with clear mocks and assertions.

packages/lib/payment/handleNoShowFee.test.ts (2)

15-23: Dynamic import mocking for PaymentServiceMap is correct.

Good approach returning a Promise that resolves to a module-like object with PaymentService.


46-54: Prisma and TeamRepository stubs are sufficient.

Stubbing prisma and injecting via mocked TeamRepository keeps tests isolated from DB concerns.

Comment on lines 72 to 85
const attendee = booking.attendees[0];

for (const attendee of booking.attendees) {
const attendeeObject = {
const attendeesList = [
{
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};

attendeesListPromises.push(attendeeObject);
}

const attendeesList = await Promise.all(attendeesListPromises);
},
];

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

Guard against empty attendees array

Accessing booking.attendees[0] without a check risks a crash. Fail fast with a clear error (or define a fallback recipient if product requirements allow).

-  const attendee = booking.attendees[0];
+  const attendee = booking.attendees?.[0];
+  if (!attendee) {
+    log.error("No attendees found on booking to process no-show fee email/locale");
+    throw new Error("No attendees found on booking");
+  }
📝 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
const attendee = booking.attendees[0];
for (const attendee of booking.attendees) {
const attendeeObject = {
const attendeesList = [
{
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};
attendeesListPromises.push(attendeeObject);
}
const attendeesList = await Promise.all(attendeesListPromises);
},
];
const attendee = booking.attendees?.[0];
if (!attendee) {
log.error("No attendees found on booking to process no-show fee email/locale");
throw new Error("No attendees found on booking");
}
const attendeesList = [
{
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
},
];
🤖 Prompt for AI Agents
In packages/lib/payment/handleNoShowFee.ts around lines 72 to 85, the code
accesses booking.attendees[0] without validating the attendees array; add a
guard that checks booking.attendees exists and has at least one element and if
not either throw a clear Error (e.g. "No attendees on booking <id>") or use the
agreed fallback recipient flow; then only construct attendeesList from
booking.attendees[0] after the guard so the function fails fast with a
descriptive message (or proceeds with the defined fallback) to avoid runtime
crashes.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 5, 2025

E2E results are ready!

Copy link
Contributor

@emrysal emrysal left a comment

Choose a reason for hiding this comment

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

🚀

@emrysal emrysal merged commit d6b17b7 into main Sep 5, 2025
40 checks passed
@emrysal emrysal deleted the devin/unit-tests-no-show-fees-1757085131 branch September 5, 2025 22:01
@coderabbitai coderabbitai bot mentioned this pull request Oct 7, 2025
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automated-tests area: unit tests, e2e tests, playwright core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO ready-for-e2e size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants