chore: platform api usage based billing using queue (#15085)

* chore: platform api usage based billing using queue

* fixup! chore: platform api usage based billing using queue

* fixup! fixup! chore: platform api usage based billing using queue

* fixup! Merge branch 'chore-platformapi-billing-queue' of github.com:calcom/cal.com into chore-platformapi-billing-queue

* fixup! fixup! Merge branch 'chore-platformapi-billing-queue' of github.com:calcom/cal.com into chore-platformapi-billing-queue

* fixup! Merge branch 'main' into chore-platformapi-billing-queue

* chore: platform billing by userId

* fix: fix tests requirement team subscirption

* fixup! Merge branch 'main' into chore-platformapi-billing-queue

* fixup! fixup! Merge branch 'main' into chore-platformapi-billing-queue

* fixup! Merge branch 'main' into chore-platformapi-billing-queue

* improve tests logs

* fix: silently close redis and queue

* fixup! Merge branch 'main' into chore-platformapi-billing-queue

* fixup! Merge branch 'main' into chore-platformapi-billing-queue

* fixup! fixup! Merge branch 'main' into chore-platformapi-billing-queue

* fix: upgrade libraries version
This commit is contained in:
Morgan
2024-07-24 17:10:22 +02:00
committed by GitHub
parent 5983c960a6
commit e8b36d5b0b
37 changed files with 347 additions and 91 deletions
+3
View File
@@ -14,8 +14,11 @@ NEXT_PUBLIC_SENTRY_DSN=
# Stripe Billing
STRIPE_PRICE_ID_STARTER=
STRIPE_PRICE_ID_STARTER_OVERAGE=
STRIPE_PRICE_ID_ESSENTIALS=
STRIPE_PRICE_ID_ESSENTIALS_OVERAGE=
STRIPE_PRICE_ID_ENTERPRISE=
STRIPE_PRICE_ID_ENTERPRISE_OVERAGE=
STRIPE_API_KEY=
STRIPE_WEBHOOK_SECRET=
+2 -1
View File
@@ -10,5 +10,6 @@
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"setupFiles": ["<rootDir>/test/setEnvVars.ts"]
"setupFiles": ["<rootDir>/test/setEnvVars.ts"],
"reporters": ["default", "jest-summarizing-reporter"]
}
+3 -1
View File
@@ -26,12 +26,13 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-libraries-0.0.20": "npm:@calcom/platform-libraries@0.0.20",
"@calcom/platform-libraries-0.0.21": "npm:@calcom/platform-libraries@0.0.21",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
"@golevelup/ts-jest": "^0.4.0",
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
"@nestjs/bull": "^10.1.1",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
@@ -42,6 +43,7 @@
"@nestjs/throttler": "^5.1.2",
"@sentry/node": "^8.8.0",
"body-parser": "^1.20.2",
"bull": "^4.12.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
+12
View File
@@ -11,6 +11,7 @@ import { JwtModule } from "@/modules/jwt/jwt.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { RedisService } from "@/modules/redis/redis.service";
import { BullModule } from "@nestjs/bull";
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_INTERCEPTOR, RouterModule } from "@nestjs/core";
@@ -26,7 +27,18 @@ import { AppController } from "./app.controller";
isGlobal: true,
load: [appConfig],
}),
RedisModule,
BullModule.forRootAsync({
imports: [RedisModule],
useFactory: async (redisService: RedisService) => ({
redis: {
host: redisService.redis.options.host,
port: redisService.redis.options.port,
},
}),
inject: [RedisService],
}),
ThrottlerModule.forRootAsync({
imports: [RedisModule],
inject: [RedisService],
@@ -19,7 +19,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { handleNewBooking } from "@calcom/platform-libraries-0.0.20";
import { handleNewBooking } from "@calcom/platform-libraries-0.0.21";
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
describe("Bookings Endpoints", () => {
@@ -46,7 +46,7 @@ import {
getBookingInfo,
handleCancelBooking,
getBookingForReschedule,
} from "@calcom/platform-libraries-0.0.20";
} from "@calcom/platform-libraries-0.0.21";
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
import { ApiResponse } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@@ -150,17 +150,21 @@ export class BookingsController {
@Req() req: BookingRequest,
@Body() body: CreateBookingInput,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<unknown>> {
): Promise<ApiResponse<Partial<BookingResponse>>> {
const oAuthClientId = clientId?.toString();
const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
const booking = await handleNewBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
);
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
if (booking.userId && booking.uid && booking.startTime) {
void (await this.billingService.increaseUsageByUserId(booking.userId, {
uid: booking.uid,
startTime: booking.startTime,
fromReschedule: booking.fromReschedule,
}));
}
return {
status: SUCCESS_STATUS,
data: booking,
@@ -177,14 +181,22 @@ export class BookingsController {
@Param("bookingId") bookingId: string,
@Body() _: CancelBookingInput,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse> {
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
const oAuthClientId = clientId?.toString();
if (bookingId) {
try {
req.body.id = parseInt(bookingId);
await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
const res = await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
if (!res.onlyRemovedAttendee) {
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
}
return {
status: SUCCESS_STATUS,
data: {
bookingId: res.bookingId,
bookingUid: res.bookingUid,
onlyRemovedAttendee: res.onlyRemovedAttendee,
},
};
} catch (err) {
this.handleBookingErrors(err);
@@ -228,7 +240,14 @@ export class BookingsController {
await this.createNextApiBookingRequest(req, oAuthClientId)
);
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
createdBookings.forEach(async (booking) => {
if (booking.userId && booking.uid && booking.startTime) {
void (await this.billingService.increaseUsageByUserId(booking.userId, {
uid: booking.uid,
startTime: booking.startTime,
}));
}
});
return {
status: SUCCESS_STATUS,
@@ -253,7 +272,15 @@ export class BookingsController {
await this.createNextApiBookingRequest(req, oAuthClientId)
);
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
if (instantMeeting.userId && instantMeeting.bookingUid) {
const now = new Date();
// add a 10 secondes delay to the usage incrementation to give some time to cancel the booking if needed
now.setSeconds(now.getSeconds() + 10);
void (await this.billingService.increaseUsageByUserId(instantMeeting.userId, {
uid: instantMeeting.bookingUid,
startTime: now,
}));
}
return {
status: SUCCESS_STATUS,
@@ -1,7 +1,7 @@
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries-0.0.20";
import type { AppsStatus } from "@calcom/platform-libraries-0.0.21";
export class CreateRecurringBookingInput extends CreateBookingInput {
@IsBoolean()
@@ -5,7 +5,7 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries-0.0.20";
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries-0.0.21";
@Injectable()
export class AppleCalendarService implements CredentialSyncCalendarApp {
@@ -18,7 +18,7 @@ import { User } from "@prisma/client";
import { DateTime } from "luxon";
import { z } from "zod";
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.20";
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.21";
import { Calendar } from "@calcom/platform-types";
import { PrismaClient } from "@calcom/prisma";
@@ -35,7 +35,7 @@ import {
EventTypesPublic,
eventTypeBookingFields,
eventTypeLocations,
} from "@calcom/platform-libraries-0.0.20";
} from "@calcom/platform-libraries-0.0.21";
import { ApiSuccessResponse } from "@calcom/platform-types";
describe("Event types Endpoints", () => {
@@ -4,7 +4,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { getEventTypeById } from "@calcom/platform-libraries-0.0.20";
import { getEventTypeById } from "@calcom/platform-libraries-0.0.21";
import type { PrismaClient } from "@calcom/prisma";
@Injectable()
@@ -14,7 +14,7 @@ import {
updateEventType,
EventTypesPublic,
getEventTypesPublic,
} from "@calcom/platform-libraries-0.0.20";
} from "@calcom/platform-libraries-0.0.21";
import { EventType } from "@calcom/prisma/client";
@Injectable()
@@ -7,7 +7,7 @@ import {
getEventTypeById,
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
} from "@calcom/platform-libraries-0.0.20";
} from "@calcom/platform-libraries-0.0.21";
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
@@ -10,9 +10,9 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.20";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.20";
import { dynamicEvent } from "@calcom/platform-libraries-0.0.20";
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.21";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.21";
import { dynamicEvent } from "@calcom/platform-libraries-0.0.21";
import {
CreateEventTypeInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
@@ -3,7 +3,7 @@ import { Injectable } from "@nestjs/common";
import {
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
} from "@calcom/platform-libraries-0.0.20";
} from "@calcom/platform-libraries-0.0.21";
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
@Injectable()
@@ -9,7 +9,7 @@ import {
parseRecurringEvent,
TransformedLocationsSchema,
BookingFieldsSchema,
} from "@calcom/platform-libraries-0.0.20";
} from "@calcom/platform-libraries-0.0.21";
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
type DatabaseEventType = EventType & EventTypeRelations;
@@ -3,7 +3,7 @@ import { Injectable } from "@nestjs/common";
import {
transformApiScheduleOverrides,
transformApiScheduleAvailability,
} from "@calcom/platform-libraries-0.0.20";
} from "@calcom/platform-libraries-0.0.21";
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
@@ -2,7 +2,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { Request } from "express";
import { ERROR_STATUS } from "@calcom/platform-constants";
import { TRPCError } from "@calcom/platform-libraries-0.0.20";
import { TRPCError } from "@calcom/platform-libraries-0.0.21";
import { Response } from "@calcom/platform-types";
@Catch(TRPCError)
@@ -1,3 +1,4 @@
import { BillingProcessor } from "@/modules/billing/billing.processor";
import { BillingRepository } from "@/modules/billing/billing.repository";
import { BillingController } from "@/modules/billing/controllers/billing.controller";
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
@@ -6,11 +7,24 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { BullModule } from "@nestjs/bull";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, StripeModule, MembershipsModule, OrganizationsModule],
providers: [BillingConfigService, BillingService, BillingRepository],
imports: [
PrismaModule,
StripeModule,
MembershipsModule,
OrganizationsModule,
BullModule.registerQueue({
name: "billing",
limiter: {
max: 1,
duration: 1000,
},
}),
],
providers: [BillingConfigService, BillingService, BillingRepository, BillingProcessor],
exports: [BillingService, BillingRepository],
controllers: [BillingController],
})
@@ -0,0 +1,91 @@
import { BillingRepository } from "@/modules/billing/billing.repository";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { StripeService } from "@/modules/stripe/stripe.service";
import { Process, Processor } from "@nestjs/bull";
import { Logger } from "@nestjs/common";
import { Job } from "bull";
export const INCREMENT_JOB = "increment";
export const BILLING_QUEUE = "billing";
export type IncrementJobDataType = {
userId: number;
};
export type DecrementJobDataType = IncrementJobDataType;
@Processor(BILLING_QUEUE)
export class BillingProcessor {
private readonly logger = new Logger(BillingProcessor.name);
constructor(
public readonly stripeService: StripeService,
private readonly billingRepository: BillingRepository,
private readonly teamsRepository: OrganizationsRepository
) {}
@Process(INCREMENT_JOB)
async handleIncrement(job: Job<IncrementJobDataType>) {
const { userId } = job.data;
try {
// get the platform organization of the managed user
const team = await this.teamsRepository.findPlatformOrgFromUserId(userId);
const teamId = team.id;
if (!team.id) {
this.logger.error(`User (${userId}) is not part of the platform organization (${teamId}) `, {
teamId,
userId,
});
return;
}
const billingSubscription = await this.billingRepository.getBillingForTeam(teamId);
if (!billingSubscription || !billingSubscription?.subscriptionId) {
this.logger.error(`Team ${teamId} did not have stripe subscription associated to it`, {
teamId,
});
return;
}
const stripeSubscription = await this.stripeService.stripe.subscriptions.retrieve(
billingSubscription.subscriptionId
);
if (!stripeSubscription?.id) {
this.logger.error(`Failed to retrieve stripe subscription (${billingSubscription.subscriptionId})`, {
teamId,
subscriptionId: billingSubscription.subscriptionId,
});
return;
}
const meteredItem = stripeSubscription.items.data.find(
(item) => item.price?.recurring?.usage_type === "metered"
);
// no metered item found to increase usage, return early
if (!meteredItem) {
this.logger.error(`Stripe subscription (${stripeSubscription.id} is not usage based`, {
teamId,
subscriptionId: stripeSubscription.id,
});
return;
}
await this.stripeService.stripe.subscriptionItems.createUsageRecord(meteredItem.id, {
action: "increment",
quantity: 1,
timestamp: "now",
});
this.logger.log("Increased organization usage for subscription", {
subscriptionId: billingSubscription.subscriptionId,
teamId,
userId,
itemId: meteredItem.id,
});
} catch (err) {
this.logger.error("Failed to increase usage for Organization", {
userId,
err,
});
}
return;
}
}
@@ -3,21 +3,38 @@ import { Injectable } from "@nestjs/common";
@Injectable()
export class BillingConfigService {
private readonly config: Map<PlatformPlan, string>;
private readonly config: Map<
PlatformPlan,
{
base: string;
overage: string;
}
>;
constructor() {
this.config = new Map<PlatformPlan, string>();
this.config = new Map<
PlatformPlan,
{
base: string;
overage: string;
}
>();
const planKeys = Object.keys(PlatformPlan).filter((key) => isNaN(Number(key)));
for (const key of planKeys) {
this.config.set(
PlatformPlan[key.toUpperCase() as keyof typeof PlatformPlan],
process.env[`STRIPE_PRICE_ID_${key}`] ?? ""
);
this.config.set(PlatformPlan[key.toUpperCase() as keyof typeof PlatformPlan], {
base: process.env[`STRIPE_PRICE_ID_${key}`] ?? "",
overage: process.env[`STRIPE_PRICE_ID_${key}_OVERAGE`] ?? "",
});
}
}
get(plan: PlatformPlan): string | undefined {
get(plan: PlatformPlan):
| {
base: string;
overage: string;
}
| undefined {
return this.config.get(plan);
}
}
@@ -1,16 +1,19 @@
import { AppConfig } from "@/config/type";
import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor";
import { BillingRepository } from "@/modules/billing/billing.repository";
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
import { PlatformPlan } from "@/modules/billing/types";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { StripeService } from "@/modules/stripe/stripe.service";
import { Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
import { InjectQueue } from "@nestjs/bull";
import { Injectable, InternalServerErrorException, Logger, OnModuleDestroy } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Queue } from "bull";
import { DateTime } from "luxon";
import Stripe from "stripe";
@Injectable()
export class BillingService {
export class BillingService implements OnModuleDestroy {
private logger = new Logger("BillingService");
private readonly webAppUrl: string;
@@ -19,9 +22,10 @@ export class BillingService {
public readonly stripeService: StripeService,
private readonly billingRepository: BillingRepository,
private readonly configService: ConfigService<AppConfig>,
private readonly billingConfigService: BillingConfigService
private readonly billingConfigService: BillingConfigService,
@InjectQueue(BILLING_QUEUE) private readonly billingQueue: Queue
) {
this.webAppUrl = configService.get("app.baseUrl", { infer: true }) ?? "https://app.cal.com";
this.webAppUrl = this.configService.get("app.baseUrl", { infer: true }) ?? "https://app.cal.com";
}
async getBillingData(teamId: number) {
@@ -58,7 +62,10 @@ export class BillingService {
customer: customerId,
line_items: [
{
price: this.billingConfigService.get(plan),
price: this.billingConfigService.get(plan)?.overage,
},
{
price: this.billingConfigService.get(plan)?.base,
quantity: 1,
},
],
@@ -99,46 +106,56 @@ export class BillingService {
);
}
async increaseUsageForTeam(teamId: number) {
try {
const billingSubscription = await this.billingRepository.getBillingForTeam(teamId);
if (!billingSubscription || !billingSubscription?.subscriptionId) {
this.logger.error("Team did not have stripe subscription associated to it", {
teamId,
});
return void 0;
}
/**
*
* Adds a job to the queue to increment usage of a stripe subscription.
* we delay the job until the booking starts.
* the delay ensure we can adapt to cancel / reschedule.
*/
async increaseUsageByUserId(
userId: number,
booking: {
uid: string;
startTime: Date;
fromReschedule?: string | null;
}
) {
const { uid, startTime, fromReschedule } = booking;
const stripeSubscription = await this.stripeService.stripe.subscriptions.retrieve(
billingSubscription.subscriptionId
);
const item = stripeSubscription.items.data[0];
// legacy plans are licensed, we cannot create usage records against them
if (item.price?.recurring?.usage_type === "licensed") {
return void 0;
}
const delay = startTime.getTime() - Date.now();
if (fromReschedule) {
// cancel the usage increment job for the booking that is being rescheduled
await this.cancelUsageByBookingUid(fromReschedule);
this.logger.log(`Cancelled usage increment job for rescheduled booking uid: ${fromReschedule}`);
}
await this.billingQueue.add(
INCREMENT_JOB,
{
userId,
} satisfies IncrementJobDataType,
{ delay: delay > 0 ? delay : 0, jobId: `increment-${uid}`, removeOnComplete: true }
);
this.logger.log(`Added stripe usage increment job for booking ${uid} and user ${userId}`);
}
await this.stripeService.stripe.subscriptionItems.createUsageRecord(item.id, {
action: "increment",
quantity: 1,
timestamp: "now",
});
} catch (error) {
// don't fail the request, log it.
this.logger.error("Failed to increase usage for team", {
teamId: teamId,
error,
});
/**
*
* Cancels the usage increment job for a booking when it is cancelled.
* Removing an attendee from a booking does not cancel the usage increment job.
*/
async cancelUsageByBookingUid(bookingUid: string) {
const job = await this.billingQueue.getJob(`increment-${bookingUid}`);
if (job) {
await job.remove();
this.logger.log(`Removed increment job for cancelled booking ${bookingUid}`);
}
}
async increaseUsageByClientId(clientId: string) {
if (this.configService.get("e2e")) {
return void 0;
async onModuleDestroy() {
try {
await this.billingQueue.close();
} catch (err) {
this.logger.error(err);
}
const team = await this.teamsRepository.findTeamIdFromClientId(clientId);
if (!team.id) return Promise.resolve(); // noop resolution.
return this.increaseUsageForTeam(team?.id);
}
}
@@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common";
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries-0.0.20";
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries-0.0.21";
@Injectable()
export class EmailService {
@@ -163,7 +163,7 @@ describe("OAuth Clients Endpoints", () => {
await app.init();
});
describe("User is not in an organization", () => {
describe("User is not part of an organization", () => {
it(`/GET`, () => {
return request(app.getHttpServer()).get("/api/v2/oauth-clients").expect(403);
});
@@ -371,6 +371,7 @@ describe("OAuth Clients Endpoints", () => {
afterAll(async () => {
await teamFixtures.delete(org.id);
await usersFixtures.delete(user.id);
await platformBillingRepositoryFixture.deleteSubscriptionForTeam(org.id);
await app.close();
});
});
@@ -7,7 +7,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable } from "@nestjs/common";
import { User } from "@prisma/client";
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries-0.0.20";
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries-0.0.21";
@Injectable()
export class OAuthClientUsersService {
@@ -70,6 +70,26 @@ export class OrganizationsRepository {
},
});
}
async findPlatformOrgFromUserId(userId: number) {
return this.dbRead.prisma.team.findFirstOrThrow({
where: {
orgProfiles: {
some: {
userId: userId,
},
},
isPlatform: true,
isOrganization: true,
},
select: {
id: true,
isPlatform: true,
isOrganization: true,
},
});
}
async findOrgUser(organizationId: number, userId: number) {
return this.dbRead.prisma.user.findUnique({
where: {
@@ -9,7 +9,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.20";
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.21";
import {
CreateTeamEventTypeInput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
@@ -6,7 +6,7 @@ import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
import { Injectable, ConflictException } from "@nestjs/common";
import { plainToInstance } from "class-transformer";
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries-0.0.20";
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries-0.0.21";
import { Team } from "@calcom/prisma/client";
@Injectable()
@@ -1,11 +1,12 @@
import { AppConfig } from "@/config/type";
import { Injectable, OnModuleDestroy } from "@nestjs/common";
import { Injectable, OnModuleDestroy, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Redis } from "ioredis";
@Injectable()
export class RedisService implements OnModuleDestroy {
public redis: Redis;
private readonly logger = new Logger("RedisService");
constructor(readonly configService: ConfigService<AppConfig>) {
const dbUrl = configService.get<string>("db.redisUrl", { infer: true });
@@ -15,6 +16,10 @@ export class RedisService implements OnModuleDestroy {
}
async onModuleDestroy() {
await this.redis.disconnect();
try {
await this.redis.quit();
} catch (err) {
this.logger.error(err);
}
}
}
@@ -5,8 +5,8 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { getAvailableSlots } from "@calcom/platform-libraries-0.0.20";
import type { AvailableSlotsType } from "@calcom/platform-libraries-0.0.20";
import { getAvailableSlots } from "@calcom/platform-libraries-0.0.21";
import type { AvailableSlotsType } from "@calcom/platform-libraries-0.0.21";
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { DateTime } from "luxon";
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries-0.0.20";
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries-0.0.21";
import { ReserveSlotInput } from "@calcom/platform-types";
@Injectable()
@@ -4,7 +4,7 @@ import { Controller, Get } from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { CityTimezones } from "@calcom/platform-libraries-0.0.20";
import type { CityTimezones } from "@calcom/platform-libraries-0.0.21";
import { ApiResponse } from "@calcom/platform-types";
@Controller({
@@ -1,8 +1,8 @@
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable } from "@nestjs/common";
import { cityTimezonesHandler } from "@calcom/platform-libraries-0.0.20";
import type { CityTimezones } from "@calcom/platform-libraries-0.0.20";
import { cityTimezonesHandler } from "@calcom/platform-libraries-0.0.21";
import type { CityTimezones } from "@calcom/platform-libraries-0.0.21";
@Injectable()
export class TimezonesService {
@@ -22,4 +22,17 @@ export class PlatformBillingRepositoryFixture {
},
});
}
async deleteSubscriptionForTeam(teamId: number) {
// silently try to delete the subscription
try {
await this.prismaWriteClient.platformBilling.delete({
where: {
id: teamId,
},
});
} catch (err) {
console.error(err);
}
}
}
+1
View File
@@ -93,6 +93,7 @@
"husky": "^8.0.0",
"i18n-unused": "^0.13.0",
"jest-diff": "^29.5.0",
"jest-summarizing-reporter": "^1.1.4",
"jsdom": "^22.0.0",
"lint-staged": "^12.5.0",
"mailhog": "^4.16.0",
@@ -144,6 +144,14 @@ export type CustomRequest = NextApiRequest & {
arePlatformEmailsEnabled?: boolean;
};
export type HandleCancelBookingResponse = {
success: boolean;
message: string;
onlyRemovedAttendee: boolean;
bookingId: number;
bookingUid: string;
};
async function handler(req: CustomRequest) {
const { id, uid, allRemainingBookings, cancellationReason, seatReferenceUid } =
schemaBookingCancelParams.parse(req.body);
@@ -312,7 +320,14 @@ async function handler(req: CustomRequest) {
// If it's just an attendee of a booking then just remove them from that booking
const result = await cancelAttendeeSeat(req, dataForWebhooks);
if (result) return { success: true };
if (result)
return {
success: true,
onlyRemovedAttendee: true,
bookingId: bookingToDelete.id,
bookingUid: bookingToDelete.uid,
message: "Attendee successfully removed.",
} satisfies HandleCancelBookingResponse;
const promises = webhooks.map((webhook) =>
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
@@ -492,7 +507,13 @@ async function handler(req: CustomRequest) {
console.error("Error deleting event", error);
}
req.statusCode = 200;
return { message: "Booking successfully cancelled." };
return {
success: true,
message: "Booking successfully cancelled.",
onlyRemovedAttendee: false,
bookingId: bookingToDelete.id,
bookingUid: bookingToDelete.uid,
} satisfies HandleCancelBookingResponse;
}
export default handler;
@@ -86,6 +86,15 @@ const handleInstantMeetingWebhookTrigger = async (args: {
}
};
export type HandleInstantMeetingResponse = {
message: string;
meetingTokenId: number;
bookingId: number;
bookingUid: string;
expires: Date;
userId: number | null;
};
async function handler(req: NextApiRequest) {
let eventType = await getEventTypesFromDB(req.body.eventTypeId);
eventType = {
@@ -247,8 +256,10 @@ async function handler(req: NextApiRequest) {
message: "Success",
meetingTokenId: instantMeetingToken.id,
bookingId: newBooking.id,
bookingUid: newBooking.uid,
expires: instantMeetingToken.expires,
};
userId: newBooking.userId,
} satisfies HandleInstantMeetingResponse;
}
export default handler;