fix: v2 sentry errors (#21214)

* fix: PATCH, POST event-types Address location must have an address

* fix: GET event-types Address location must have an address

* fix: POST /v2/bookings Address location must have an address

* refactor: GET /v2/slots Invalid time range given.

* fix: POST v2/event-types Cannot read properties of null (reading 'type')

* fix: POST /v2/bookings/:bookingUid/mark-absent Failed to update no-show status
This commit is contained in:
Lauris Skraucis authored and GitHub committed 2025-05-14 10:11:35 +03:00
1 parent 39077a08f1
commit 1a70f83d34
8 files changed
+111 -51

No files matched your search

@@ -20,6 +20,7 @@ import { UsersRepository, UserWithProfile } from "@/modules/users/users.reposito
import { ConflictException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { BadRequestException } from "@nestjs/common";
import { Request } from "express";
import { DateTime } from "luxon";
import { z } from "zod";
import {
@@ -594,6 +595,20 @@ export class BookingsService_2024_08_13 {
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
if (!bookingBefore) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found.`);
}
const nowUtc = DateTime.utc();
const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" });
if (nowUtc < bookingStartTimeUtc) {
throw new BadRequestException(
`Bookings can only be marked as absent after their scheduled start time. Current time in UTC+0: ${nowUtc.toISO()}, Booking start time in UTC+0: ${bookingStartTimeUtc.toISO()}`
);
}
const platformClientParams = bookingBefore?.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;
@@ -1,7 +1,7 @@
import { SlotsOutputService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
import type { RangeSlots, TimeSlots } from "@/modules/slots/slots-2024-04-15/services/slots-output.service";
import { SlotsService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots.service";
import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common";
import { Query, Body, Controller, Get, Delete, Post, Req, Res, BadRequestException } from "@nestjs/common";
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger";
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
@@ -157,6 +157,7 @@ export class SlotsController_2024_04_15 {
@Query() query: GetAvailableSlotsInput_2024_04_15,
@Req() req: ExpressRequest
): Promise<ApiResponse<{ slots: TimeSlots["slots"] | RangeSlots["slots"] }>> {
try {
const isTeamEvent =
query.isTeamEvent === undefined
? await this.slotsService.checkIfIsTeamEvent(query.eventTypeId)
@@ -171,19 +172,29 @@ export class SlotsController_2024_04_15 {
},
});
const { slots } = await this.slotsOutputService.getOutputSlots(
availableSlots,
query.duration,
query.eventTypeId,
query.slotFormat,
query.timeZone
);
const { slots } = await this.slotsOutputService.getOutputSlots(
availableSlots,
query.duration,
query.eventTypeId,
query.slotFormat,
query.timeZone
);
return {
data: {
slots,
},
status: SUCCESS_STATUS,
};
return {
data: {
slots,
},
status: SUCCESS_STATUS,
};
} catch (error) {
if (error instanceof Error) {
if (error.message.includes("Invalid time range given")) {
throw new BadRequestException(
"Invalid time range given - check the 'startTime' and 'endTime' query parameters."
);
}
}
throw error;
}
}
}
@@ -42,24 +42,33 @@ export class SlotsService_2024_09_04 {
) {}
async getAvailableSlots(query: GetSlotsInput_2024_09_04) {
const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query);
try {
const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query);
const availableSlots: TimeSlots = await getAvailableSlots({
input: {
...queryTransformed,
},
ctx: {},
});
const formatted = await this.slotsOutputService.getAvailableSlots(
availableSlots,
queryTransformed.eventTypeId,
queryTransformed.duration,
query.format,
queryTransformed.timeZone
);
const availableSlots: TimeSlots = await getAvailableSlots({
input: {
...queryTransformed,
},
ctx: {},
});
const formatted = await this.slotsOutputService.getAvailableSlots(
availableSlots,
queryTransformed.eventTypeId,
queryTransformed.duration,
query.format,
queryTransformed.timeZone
);
return formatted;
return formatted;
} catch (error) {
if (error instanceof Error) {
if (error.message.includes("Invalid time range given")) {
throw new BadRequestException(
"Invalid time range given - check the 'start' and 'end' query parameters."
);
}
}
throw error;
}
}
async reserveSlot(input: ReserveSlotInput_2024_09_04, authUserId?: number) {
@@ -9,6 +9,7 @@ import type {
OutputLinkLocation_2024_06_14,
OutputPhoneLocation_2024_06_14,
OutputUnknownLocation_2024_06_14,
OutputLocation_2024_06_14,
} from "@calcom/platform-types";
import type { InternalLocation } from "../internal/locations";
@@ -50,64 +51,73 @@ export function transformLocationsInternalToApi(internalLocations: InternalLocat
return [];
}
return internalLocations.map((location) => {
const apiLocations: OutputLocation_2024_06_14[] = [];
for (const location of internalLocations) {
switch (location.type) {
case "inPerson": {
if (!location.address) {
throw new Error("Address location must have an address");
continue;
}
const addressLocation: OutputAddressLocation_2024_06_14 = {
type: "address",
address: location.address,
public: location.displayLocationPublicly,
};
return addressLocation;
apiLocations.push(addressLocation);
break;
}
case "attendeeInPerson": {
const attendeeAddressLocation: OutputAttendeeAddressLocation_2024_06_14 = {
type: "attendeeAddress",
};
return attendeeAddressLocation;
apiLocations.push(attendeeAddressLocation);
break;
}
case "link": {
if (!location.link) {
throw new Error("Link location must have a link");
continue;
}
const linkLocation: OutputLinkLocation_2024_06_14 = {
type: "link",
link: location.link,
public: location.displayLocationPublicly,
};
return linkLocation;
apiLocations.push(linkLocation);
break;
}
case "userPhone": {
if (!location.hostPhoneNumber) {
throw new Error("Phone location must have a phone number");
continue;
}
const phoneLocation: OutputPhoneLocation_2024_06_14 = {
type: "phone",
phone: location.hostPhoneNumber,
public: location.displayLocationPublicly,
};
return phoneLocation;
apiLocations.push(phoneLocation);
break;
}
case "phone": {
const attendeePhoneLocation: OutputAttendeePhoneLocation_2024_06_14 = {
type: "attendeePhone",
};
return attendeePhoneLocation;
apiLocations.push(attendeePhoneLocation);
break;
}
case "somewhereElse": {
const attendeeDefinedLocation: OutputAttendeeDefinedLocation_2024_06_14 = {
type: "attendeeDefined",
};
return attendeeDefinedLocation;
apiLocations.push(attendeeDefinedLocation);
break;
}
case "conferencing": {
const conferencingLocation: OutputOrganizersDefaultAppLocation_2024_06_14 = {
type: "organizersDefaultApp",
};
return conferencingLocation;
apiLocations.push(conferencingLocation);
break;
}
default: {
const integrationType = internalToApiIntegrationsMapping[location.type];
@@ -116,7 +126,8 @@ export function transformLocationsInternalToApi(internalLocations: InternalLocat
type: "unknown",
location: JSON.stringify(location),
};
return unknown;
apiLocations.push(unknown);
break;
}
const integration: OutputIntegrationLocation_2024_06_14 = {
type: "integration",
@@ -124,8 +135,11 @@ export function transformLocationsInternalToApi(internalLocations: InternalLocat
link: location.link,
credentialId: location.credentialId,
};
return integration;
apiLocations.push(integration);
break;
}
}
});
}
return apiLocations;
}
@@ -1,6 +1,6 @@
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { IsString, IsIn, IsPhoneNumber } from "class-validator";
import { IsString, IsIn, IsPhoneNumber, MinLength } from "class-validator";
import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator";
import { registerDecorator, validate, ValidatorConstraint } from "class-validator";
@@ -104,6 +104,7 @@ export class BookingInputAttendeeAddressLocation_2024_08_13 {
type!: "attendeeAddress";
@IsString()
@MinLength(1)
@DocsProperty({ example: "123 Example St, City, Country" })
address!: string;
}
@@ -126,6 +127,7 @@ export class BookingInputAttendeeDefinedLocation_2024_08_13 {
type!: "attendeeDefined";
@IsString()
@MinLength(1)
@DocsProperty({ example: "321 Example St, City, Country" })
location!: string;
}
@@ -1,7 +1,7 @@
import { BadRequestException } from "@nestjs/common";
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { IsString, IsUrl, IsIn, IsPhoneNumber, IsBoolean } from "class-validator";
import { IsString, IsUrl, IsIn, IsPhoneNumber, IsBoolean, MinLength } from "class-validator";
import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator";
import { registerDecorator, validate, ValidatorConstraint } from "class-validator";
@@ -22,6 +22,7 @@ export class InputAddressLocation_2024_06_14 {
type!: "address";
@IsString()
@MinLength(1)
@DocsProperty({ example: "123 Example St, City, Country" })
address!: string;
@@ -136,6 +137,10 @@ class InputLocationValidator_2024_06_14 implements ValidatorConstraintInterface
}
for (const location of locations) {
if (!location || typeof location !== "object") {
throw new BadRequestException(`Each object in 'locations' must be an object.`);
}
const { type } = location;
if (!type) {
throw new BadRequestException(`Each object in 'locations' must have a 'type' property.`);
@@ -187,6 +192,10 @@ class InputTeamLocationValidator_2024_06_14 implements ValidatorConstraintInterf
}
for (const location of locations) {
if (!location || typeof location !== "object") {
throw new BadRequestException(`Each object in 'locations' must be an object.`);
}
const { type } = location;
if (!type) {
throw new BadRequestException(`Each object in 'locations' must have a 'type' property.`);
@@ -15,14 +15,14 @@ import {
import { SlotFormat } from "@calcom/platform-enums";
export class GetAvailableSlotsInput_2024_04_15 {
@IsDateString()
@IsDateString({ strict: true })
@ApiProperty({
description: "Start date string starting from which to fetch slots in UTC timezone.",
example: "2022-06-14T00:00:00.000Z",
})
startTime!: string;
@IsDateString()
@IsDateString({ strict: true })
@ApiProperty({
description: "End date string until which to fetch slots in UTC timezone.",
example: "2022-06-14T23:59:59.999Z",
@@ -14,7 +14,7 @@ import {
import { SlotFormat } from "@calcom/platform-enums";
export class GetAvailableSlotsInput_2024_09_04 {
@IsDateString()
@IsDateString({ strict: true })
@ApiProperty({
type: String,
description: `
@@ -29,7 +29,7 @@ export class GetAvailableSlotsInput_2024_09_04 {
})
start!: string;
@IsDateString()
@IsDateString({ strict: true })
@ApiProperty({
type: String,
description: `