Skip to content

Commit 6eafb4b

Browse files
feat: skip platform billing for non-platform-managed users (calcom#27521)
* feat: skip platform billing for non-platform-managed users - Add isPlatformManaged to user select in saveBooking and findBookingQuery - Update 2024-04-15 booking controller to check isPlatformManaged before billing - Update 2024-08-13 bookings service billBooking methods to check isPlatformManaged - Update buildDryRunBooking to include isPlatformManaged in user object Co-Authored-By: morgan@cal.com <morgan@cal.com> * test: update buildDryRunBooking test to include uuid and isPlatformManaged fields Co-Authored-By: morgan@cal.com <morgan@cal.com> * refactor: simplify to only check isPlatformManaged in normal booking flow Co-Authored-By: morgan@cal.com <morgan@cal.com> * test: add E2E tests for billing behavior based on isPlatformManaged flag Co-Authored-By: morgan@cal.com <morgan@cal.com> * chore: only trigger platform billing for platform user bookingd * test: add E2E tests for cancel and recurring booking billing behavior Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: correct expected status code for cancel booking endpoint (201 instead of 200) Co-Authored-By: morgan@cal.com <morgan@cal.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 19e89df commit 6eafb4b

9 files changed

Lines changed: 632 additions & 114 deletions

File tree

apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings-billing.e2e-spec.ts

Lines changed: 542 additions & 0 deletions
Large diffs are not rendered by default.

apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts

Lines changed: 57 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,59 @@
1+
import {
2+
BOOKING_READ,
3+
BOOKING_WRITE,
4+
SUCCESS_STATUS,
5+
X_CAL_CLIENT_ID,
6+
X_CAL_PLATFORM_EMBED,
7+
} from "@calcom/platform-constants";
8+
import {
9+
BookingResponse,
10+
CreationSource,
11+
getAllUserBookings,
12+
getBookingForReschedule,
13+
getBookingInfo,
14+
handleCancelBooking,
15+
handleMarkNoShow,
16+
} from "@calcom/platform-libraries";
17+
import { type InstantBookingCreateResult } from "@calcom/platform-libraries/bookings";
18+
import { ErrorCode, HttpError } from "@calcom/platform-libraries/errors";
19+
import type { ApiResponse } from "@calcom/platform-types";
20+
import {
21+
CancelBookingInput_2024_04_15,
22+
GetBookingsInput_2024_04_15,
23+
Status_2024_04_15,
24+
} from "@calcom/platform-types";
25+
import type { PrismaClient } from "@calcom/prisma";
26+
import {
27+
BadRequestException,
28+
Body,
29+
Controller,
30+
ForbiddenException,
31+
Get,
32+
Headers,
33+
HttpException,
34+
InternalServerErrorException,
35+
Logger,
36+
NotFoundException,
37+
Param,
38+
Post,
39+
Query,
40+
Req,
41+
UnauthorizedException,
42+
UseGuards,
43+
} from "@nestjs/common";
44+
import { ConfigService } from "@nestjs/config";
45+
import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
46+
import { Request } from "express";
47+
import { NextApiRequest } from "next/types";
48+
import { v4 as uuidv4 } from "uuid";
149
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
250
import { CreateRecurringBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-recurring-booking.input";
351
import { MarkNoShowInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/mark-no-show.input";
452
import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output";
553
import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
654
import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output";
755
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
8-
import { sha256Hash, isApiKey, stripApiKey } from "@/lib/api-key";
56+
import { isApiKey, sha256Hash, stripApiKey } from "@/lib/api-key";
957
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
1058
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
1159
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
@@ -30,50 +78,6 @@ import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.se
3078
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
3179
import { UsersService } from "@/modules/users/services/users.service";
3280
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
33-
import {
34-
Controller,
35-
Post,
36-
Logger,
37-
Req,
38-
InternalServerErrorException,
39-
Body,
40-
Headers,
41-
HttpException,
42-
Param,
43-
Get,
44-
Query,
45-
NotFoundException,
46-
UseGuards,
47-
BadRequestException,
48-
UnauthorizedException,
49-
ForbiddenException,
50-
} from "@nestjs/common";
51-
import { ConfigService } from "@nestjs/config";
52-
import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
53-
import { Request } from "express";
54-
import { NextApiRequest } from "next/types";
55-
import { v4 as uuidv4 } from "uuid";
56-
57-
import { X_CAL_CLIENT_ID, X_CAL_PLATFORM_EMBED } from "@calcom/platform-constants";
58-
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
59-
import {
60-
BookingResponse,
61-
handleMarkNoShow,
62-
getAllUserBookings,
63-
getBookingInfo,
64-
handleCancelBooking,
65-
getBookingForReschedule,
66-
} from "@calcom/platform-libraries";
67-
import { CreationSource } from "@calcom/platform-libraries";
68-
import { type InstantBookingCreateResult } from "@calcom/platform-libraries/bookings";
69-
import { HttpError, ErrorCode } from "@calcom/platform-libraries/errors";
70-
import {
71-
GetBookingsInput_2024_04_15,
72-
CancelBookingInput_2024_04_15,
73-
Status_2024_04_15,
74-
} from "@calcom/platform-types";
75-
import type { ApiResponse } from "@calcom/platform-types";
76-
import type { PrismaClient } from "@calcom/prisma";
7781

7882
type BookingRequest = Request & {
7983
userId?: number;
@@ -137,7 +141,7 @@ export class BookingsController_2024_04_15 {
137141
@Query() queryParams: GetBookingsInput_2024_04_15
138142
): Promise<GetBookingsOutput_2024_04_15> {
139143
const { filters, cursor, limit } = queryParams;
140-
const bookingListingByStatus = filters?.status ?? Status_2024_04_15["upcoming"];
144+
const bookingListingByStatus = filters?.status ?? Status_2024_04_15.upcoming;
141145
const profile = this.usersService.getUserMainProfile(user);
142146
const bookings = await getAllUserBookings({
143147
bookingListingByStatus: [bookingListingByStatus],
@@ -221,7 +225,7 @@ export class BookingsController_2024_04_15 {
221225
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
222226
},
223227
});
224-
if (booking.userId && booking.uid && booking.startTime) {
228+
if (booking.userId && booking.uid && booking.startTime && booking.user?.isPlatformManaged) {
225229
void (await this.billingService.increaseUsageByUserId(booking.userId, {
226230
uid: booking.uid,
227231
startTime: booking.startTime,
@@ -242,12 +246,12 @@ export class BookingsController_2024_04_15 {
242246
async cancelBooking(
243247
@Req() req: BookingRequest,
244248
@Param("bookingUid") bookingUid: string,
245-
@Body() body: CancelBookingInput_2024_04_15,
249+
@Body() _body: CancelBookingInput_2024_04_15,
246250
@Headers(X_CAL_CLIENT_ID) clientId?: string,
247251
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
248252
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
249253
const oAuthClientId = clientId?.toString();
250-
const isUidNumber = !isNaN(Number(bookingUid));
254+
const isUidNumber = !Number.isNaN(Number(bookingUid));
251255

252256
if (isUidNumber) {
253257
throw new BadRequestException("Please provide booking uid instead of booking id.");
@@ -276,7 +280,7 @@ export class BookingsController_2024_04_15 {
276280
platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
277281
platformBookingUrl: bookingRequest.platformBookingUrl,
278282
});
279-
if (!res.onlyRemovedAttendee) {
283+
if (!res.onlyRemovedAttendee && res.isPlatformManagedUserBooking) {
280284
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
281285
}
282286
return {
@@ -353,7 +357,7 @@ export class BookingsController_2024_04_15 {
353357
});
354358

355359
createdBookings.forEach(async (booking) => {
356-
if (booking.userId && booking.uid && booking.startTime) {
360+
if (booking.userId && booking.uid && booking.startTime && booking.user.isPlatformManaged) {
357361
void (await this.billingService.increaseUsageByUserId(booking.userId, {
358362
uid: booking.uid,
359363
startTime: booking.startTime,
@@ -623,58 +627,22 @@ export class BookingsController_2024_04_15 {
623627
oAuthClientId
624628
);
625629
}
626-
if (requestBody?.responses?.guests && requestBody?.responses?.guests.length) {
630+
if (requestBody?.responses?.guests?.length) {
627631
requestBody.responses.guests = await this.platformBookingsService.getPlatformAttendeesEmails(
628632
requestBody.responses.guests,
629633
oAuthClientId
630634
);
631635
}
632636
}
633637

634-
private async createNextApiRecurringBookingRequest(
635-
req: BookingRequest,
636-
oAuthClientId?: string,
637-
platformBookingLocation?: string,
638-
isEmbed?: string
639-
): Promise<NextApiRequest & { userId?: number; userUuid?: string } & OAuthRequestParams> {
640-
const clone = { ...req };
641-
const owner = await this.getOwner(req);
642-
const userId = owner?.id ?? -1;
643-
const userUuid = owner?.uuid;
644-
645-
const oAuthParams = oAuthClientId
646-
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
647-
: DEFAULT_PLATFORM_PARAMS;
648-
const requestId = req.get("X-Request-Id");
649-
this.logger.log(`createNextApiRecurringBookingRequest_2024_04_15`, {
650-
requestId,
651-
ownerId: userId,
652-
platformBookingLocation,
653-
oAuthClientId,
654-
...oAuthParams,
655-
});
656-
Object.assign(clone, {
657-
userId,
658-
userUuid,
659-
...oAuthParams,
660-
platformBookingLocation,
661-
noEmail: !oAuthParams.arePlatformEmailsEnabled,
662-
creationSource: CreationSource.API_V2,
663-
});
664-
if (oAuthClientId) {
665-
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
666-
}
667-
return clone as unknown as NextApiRequest & { userId?: number; userUuid?: string } & OAuthRequestParams;
668-
}
669-
670638
private handleBookingErrors(
671639
err: Error | HttpError | unknown,
672640
type?: "recurring" | `instant` | "no-show"
673641
): void {
674642
const errMsg =
675643
type === "no-show"
676644
? `Error while marking no-show.`
677-
: `Error while creating ${type ? type + " " : ""}booking.`;
645+
: `Error while creating ${type ? `${type} ` : ""}booking.`;
678646
if (err instanceof HttpError) {
679647
const httpError = err as HttpError;
680648
throw new HttpException(httpError?.message ?? errMsg, httpError?.statusCode ?? 500);

packages/features/bookings/lib/dto/BookingCancel.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ export type HandleCancelBookingResponse = {
3232
onlyRemovedAttendee: boolean;
3333
bookingId: number;
3434
bookingUid: string;
35+
isPlatformManagedUserBooking: boolean;
3536
};

packages/features/bookings/lib/getBookingToDelete.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export async function getBookingToDelete(id: number | undefined, uid: string | u
2323
name: true,
2424
destinationCalendar: true,
2525
locale: true,
26+
isPlatformManaged: true,
2627
profiles: {
2728
select: {
2829
organizationId: true,

packages/features/bookings/lib/handleCancelBooking.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import type { z } from "zod";
2-
import { v4 as uuidv4 } from "uuid";
3-
41
import { DailyLocationType } from "@calcom/app-store/constants";
52
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
63
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
74
import dayjs from "@calcom/dayjs";
85
import { sendCancelledEmailsAndSMS } from "@calcom/emails/email-manager";
6+
import type { Actor } from "@calcom/features/booking-audit/lib/dto/types";
7+
import {
8+
buildActorEmail,
9+
getUniqueIdentifier,
10+
makeGuestActor,
11+
makeUserActor,
12+
} from "@calcom/features/booking-audit/lib/makeActor";
913
import type { ActionSource } from "@calcom/features/booking-audit/lib/types/actionSource";
14+
import { BookingReferenceRepository } from "@calcom/features/bookingReference/repositories/BookingReferenceRepository";
1015
import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container";
1116
import EventManager from "@calcom/features/bookings/lib/EventManager";
1217
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
@@ -23,8 +28,8 @@ import { UserRepository } from "@calcom/features/users/repositories/UserReposito
2328
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
2429
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
2530
import {
26-
deleteWebhookScheduledTriggers,
2731
cancelNoShowTasksForBooking,
32+
deleteWebhookScheduledTriggers,
2833
} from "@calcom/features/webhooks/lib/scheduleTrigger";
2934
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
3035
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
@@ -36,36 +41,28 @@ import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
3641
import logger from "@calcom/lib/logger";
3742
import { safeStringify } from "@calcom/lib/safeStringify";
3843
import { getTranslation } from "@calcom/lib/server/i18n";
39-
import { BookingReferenceRepository } from "@calcom/features/bookingReference/repositories/BookingReferenceRepository";
4044
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
4145
// TODO: Prisma import would be used from DI in a followup PR when we remove `handler` export
4246
import prisma from "@calcom/prisma";
43-
import type { WorkflowMethods } from "@calcom/prisma/enums";
44-
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
47+
import type { WebhookTriggerEvents, WorkflowMethods } from "@calcom/prisma/enums";
4548
import { BookingStatus } from "@calcom/prisma/enums";
46-
import { bookingMetadataSchema, bookingCancelInput } from "@calcom/prisma/zod-utils";
4749
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
50+
import { bookingCancelInput, bookingMetadataSchema } from "@calcom/prisma/zod-utils";
4851
import type { CalendarEvent } from "@calcom/types/Calendar";
49-
52+
import { v4 as uuidv4 } from "uuid";
53+
import type { z } from "zod";
5054
import { BookingRepository } from "../repositories/BookingRepository";
5155
import { PrismaBookingAttendeeRepository } from "../repositories/PrismaBookingAttendeeRepository";
5256
import type {
53-
CancelRegularBookingData,
5457
CancelBookingMeta,
58+
CancelRegularBookingData,
5559
HandleCancelBookingResponse,
5660
} from "./dto/BookingCancel";
5761
import { getAllCredentialsIncludeServiceAccountKey } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
5862
import { getBookingToDelete } from "./getBookingToDelete";
5963
import { handleInternalNote } from "./handleInternalNote";
6064
import cancelAttendeeSeat from "./handleSeats/cancel/cancelAttendeeSeat";
6165
import type { IBookingCancelService } from "./interfaces/IBookingCancelService";
62-
import {
63-
buildActorEmail,
64-
getUniqueIdentifier,
65-
makeGuestActor,
66-
makeUserActor,
67-
} from "@calcom/features/booking-audit/lib/makeActor";
68-
import type { Actor } from "@calcom/features/booking-audit/lib/dto/types";
6966

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

@@ -218,7 +215,7 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) {
218215
}
219216

220217
const isCancellationUserHost =
221-
bookingToDelete.userId == userId || bookingToDelete.user.email === cancelledBy;
218+
bookingToDelete.userId === userId || bookingToDelete.user.email === cancelledBy;
222219

223220
if (
224221
!platformClientId &&
@@ -385,12 +382,12 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) {
385382
cancellationReason: cancellationReason,
386383
...(teamMembers &&
387384
teamId && {
388-
team: {
389-
name: bookingToDelete?.eventType?.team?.name || "Nameless",
390-
members: teamMembers,
391-
id: teamId,
392-
},
393-
}),
385+
team: {
386+
name: bookingToDelete?.eventType?.team?.name || "Nameless",
387+
members: teamMembers,
388+
id: teamId,
389+
},
390+
}),
394391
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
395392
seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees,
396393
iCalUID: bookingToDelete.iCalUID,
@@ -423,6 +420,7 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) {
423420
bookingId: bookingToDelete.id,
424421
bookingUid: bookingToDelete.uid,
425422
message: "Attendee successfully removed.",
423+
isPlatformManagedUserBooking: bookingToDelete.user.isPlatformManaged,
426424
} satisfies HandleCancelBookingResponse;
427425

428426
const promises = webhooks.map((webhook) =>
@@ -698,6 +696,7 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) {
698696
onlyRemovedAttendee: false,
699697
bookingId: bookingToDelete.id,
700698
bookingUid: bookingToDelete.uid,
699+
isPlatformManagedUserBooking: bookingToDelete.user.isPlatformManaged,
701700
} satisfies HandleCancelBookingResponse;
702701
}
703702

packages/features/bookings/lib/handleNewBooking/createBooking.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ async function saveBooking(
143143
const createBookingObj = {
144144
include: {
145145
user: {
146-
select: { uuid: true, email: true, name: true, timeZone: true, username: true },
146+
select: { uuid: true, email: true, name: true, timeZone: true, username: true, isPlatformManaged: true },
147147
},
148148
attendees: true,
149149
payment: true,

packages/features/bookings/lib/handleNewBooking/findBookingQuery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const _findBookingQuery = async (bookingId: number) => {
2424
email: true,
2525
timeZone: true,
2626
username: true,
27+
isPlatformManaged: true,
2728
},
2829
},
2930
eventType: {

0 commit comments

Comments
 (0)