fix: Re-instate new slot logic now it's A/B tested in production (#19605)

* fix: Re-instate new slot logic now it's well tested

* Fixup getAggregatedAvailability

* Changes to code-owners to reflect removed/moved code

* Move more files around.

* Fix failed type

---------

Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Alex van AndelandOmar López authored and GitHub committed 2025-03-02 23:32:31 +00:00
1 parent 4bab8c2c32
commit 1cfd94d293
13 files changed
+21 -881

No files matched your search

@@ -1,340 +0,0 @@
import { describe, it, expect } from "vitest";
import type { WorkingHours } from "@calcom/types/schedule";
import { getAggregateWorkingHours } from "./getAggregateWorkingHours";
describe("getAggregateWorkingHours", () => {
it("should return all schedules if no scheduling type", () => {
const workingHours: WorkingHours[] = [
{
days: [1, 2, 3],
startTime: 0,
endTime: 720,
},
];
const result = getAggregateWorkingHours(
[
{
busy: [],
timeZone: "Europe/London",
workingHours,
dateOverrides: [],
datesOutOfOffice: {},
},
{
busy: [],
timeZone: "Europe/London",
workingHours,
dateOverrides: [],
datesOutOfOffice: {},
},
],
null
);
expect(result).toEqual([...workingHours, ...workingHours]);
});
it("should return all schedules if no fixed users exist", () => {
const workingHours: WorkingHours[] = [
{
days: [1, 2, 3],
startTime: 0,
endTime: 720,
},
];
const result = getAggregateWorkingHours(
[
{
busy: [],
timeZone: "Europe/London",
workingHours,
dateOverrides: [],
datesOutOfOffice: {},
},
{
busy: [],
timeZone: "Europe/London",
workingHours,
dateOverrides: [],
datesOutOfOffice: {},
},
],
"MANAGED"
);
expect(result).toEqual([...workingHours, ...workingHours]);
});
it("should consider all schedules fixed if collective", () => {
const workingHoursA: WorkingHours[] = [
{
days: [1, 2],
startTime: 0,
endTime: 200,
},
];
const workingHoursB: WorkingHours[] = [
{
days: [2, 3],
startTime: 100,
endTime: 300,
},
];
const result = getAggregateWorkingHours(
[
{
busy: [],
timeZone: "Europe/London",
workingHours: workingHoursA,
dateOverrides: [],
datesOutOfOffice: {},
user: {
isFixed: false,
},
},
{
busy: [],
timeZone: "Europe/London",
workingHours: workingHoursB,
dateOverrides: [],
datesOutOfOffice: {},
user: {
isFixed: false,
},
},
],
"COLLECTIVE"
);
expect(result).toEqual([
{
days: [2],
startTime: 100,
endTime: 200,
},
]);
});
it("should include loose host hours", () => {
const workingHoursA: WorkingHours[] = [
{
days: [1, 2],
startTime: 0,
endTime: 200,
},
];
const workingHoursB: WorkingHours[] = [
{
days: [2, 3],
startTime: 100,
endTime: 300,
},
];
const result = getAggregateWorkingHours(
[
{
busy: [],
timeZone: "Europe/London",
workingHours: workingHoursA,
dateOverrides: [],
datesOutOfOffice: {},
user: {
isFixed: false,
},
},
{
busy: [],
timeZone: "Europe/London",
workingHours: workingHoursB,
dateOverrides: [],
datesOutOfOffice: {},
user: {
isFixed: true,
},
},
],
"COLLECTIVE"
);
expect(result).toEqual([
{
days: [2],
startTime: 100,
endTime: 200,
userId: undefined,
},
]);
});
it("should return last user's hours if no intersection", () => {
const workingHoursA: WorkingHours[] = [
{
days: [1],
startTime: 0,
endTime: 200,
},
];
const workingHoursB: WorkingHours[] = [
{
days: [2],
startTime: 100,
endTime: 300,
},
];
const result = getAggregateWorkingHours(
[
{
busy: [],
timeZone: "Europe/London",
workingHours: workingHoursA,
dateOverrides: [],
datesOutOfOffice: {},
user: {
isFixed: true,
},
},
{
busy: [],
timeZone: "Europe/London",
workingHours: workingHoursB,
dateOverrides: [],
datesOutOfOffice: {},
user: {
isFixed: true,
},
},
],
"COLLECTIVE"
);
expect(result).toEqual([...workingHoursB]);
});
it("should include user IDs when not collective", () => {
const workingHoursA: WorkingHours[] = [
{
days: [1, 2],
startTime: 0,
endTime: 200,
userId: 1,
},
];
const workingHoursB: WorkingHours[] = [
{
days: [2, 3],
startTime: 100,
endTime: 300,
userId: 2,
},
];
const result = getAggregateWorkingHours(
[
{
busy: [],
timeZone: "Europe/London",
workingHours: workingHoursA,
dateOverrides: [],
datesOutOfOffice: {},
user: {
isFixed: true,
},
},
{
busy: [],
timeZone: "Europe/London",
workingHours: workingHoursB,
dateOverrides: [],
datesOutOfOffice: {},
user: {
isFixed: true,
},
},
],
"MANAGED"
);
expect(result).toEqual([
{
userId: 1,
days: [2],
startTime: 100,
endTime: 200,
},
]);
});
it("should handle multiple intersections", () => {
const workingHoursA: WorkingHours[] = [
{
days: [1, 2],
startTime: 0,
endTime: 200,
},
{
days: [3, 4],
startTime: 100,
endTime: 300,
},
];
const workingHoursB: WorkingHours[] = [
{
days: [2, 3],
startTime: 100,
endTime: 300,
},
{
days: [4, 5],
startTime: 0,
endTime: 200,
},
];
const result = getAggregateWorkingHours(
[
{
busy: [],
timeZone: "Europe/London",
workingHours: workingHoursA,
dateOverrides: [],
datesOutOfOffice: {},
user: {
isFixed: true,
},
},
{
busy: [],
timeZone: "Europe/London",
workingHours: workingHoursB,
dateOverrides: [],
datesOutOfOffice: {},
user: {
isFixed: true,
},
},
],
"COLLECTIVE"
);
expect(result).toEqual([
{
days: [2],
startTime: 100,
endTime: 200,
userId: undefined,
},
{
days: [3],
startTime: 100,
endTime: 300,
userId: undefined,
},
{
days: [4],
startTime: 100,
endTime: 200,
userId: undefined,
},
]);
});
});
-60
View File
@@ -1,60 +0,0 @@
import { SchedulingType } from "@calcom/prisma/enums";
import type { WorkingHours } from "@calcom/types/schedule";
/**
* This function gets team members working hours and busy slots,
* offsets them to UTC and intersects them for collective events.
**/
export const getAggregateWorkingHours = (
usersWorkingHoursAndBusySlots: (Omit<
Awaited<ReturnType<Awaited<typeof import("./getUserAvailability")>["getUserAvailability"]>>,
"currentSeats" | "dateRanges" | "oooExcludedDateRanges"
> & { user?: { isFixed?: boolean } })[],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
schedulingType: SchedulingType | null
): WorkingHours[] => {
// during personal events, just flatMap.
if (!schedulingType) {
return usersWorkingHoursAndBusySlots.flatMap((s) => s.workingHours);
}
const looseHostWorkingHours = usersWorkingHoursAndBusySlots
.filter(({ user }) => schedulingType !== SchedulingType.COLLECTIVE && user?.isFixed !== true)
.flatMap((s) => s.workingHours);
const fixedHostSchedules = usersWorkingHoursAndBusySlots.filter(
({ user }) => schedulingType === SchedulingType.COLLECTIVE || user?.isFixed
);
// return early when there are no fixed hosts.
if (!fixedHostSchedules.length) {
return looseHostWorkingHours;
}
return fixedHostSchedules.reduce((currentWorkingHours: WorkingHours[], s) => {
const updatedWorkingHours: typeof currentWorkingHours = [];
s.workingHours.forEach((workingHour) => {
const sameDayWorkingHours = currentWorkingHours.filter((compare) =>
compare.days.find((day) => workingHour.days.includes(day))
);
if (!sameDayWorkingHours.length) {
updatedWorkingHours.push(workingHour); // the first day is always added.
return;
}
// days are overlapping when different users are involved, instead of adding we now need to subtract
updatedWorkingHours.push(
...sameDayWorkingHours.map((compare) => {
const intersect = workingHour.days.filter((day) => compare.days.includes(day));
const retVal: WorkingHours & { userId?: number | null } = {
days: intersect,
startTime: Math.max(workingHour.startTime, compare.startTime),
endTime: Math.min(workingHour.endTime, compare.endTime),
};
if (schedulingType !== SchedulingType.COLLECTIVE) {
retVal.userId = compare.userId;
}
return retVal;
})
);
});
return updatedWorkingHours;
}, looseHostWorkingHours);
};
@@ -3,7 +3,7 @@ import { describe, it, expect } from "vitest";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { getAggregatedAvailabilityNew as getAggregatedAvailability } from ".";
import { getAggregatedAvailability } from "./getAggregatedAvailability";
// Helper to check if a time range overlaps with availability
const isAvailable = (availability: { start: Dayjs; end: Dayjs }[], range: { start: Dayjs; end: Dayjs }) => {
@@ -2,7 +2,7 @@ import type { DateRange } from "@calcom/lib/date-ranges";
import { intersect } from "@calcom/lib/date-ranges";
import { SchedulingType } from "@calcom/prisma/enums";
import { mergeOverlappingDateRanges } from "./date-range-utils/mergeOverlappingDateRanges";
import { mergeOverlappingDateRanges } from "./getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges";
function uniqueAndSortedDateRanges(ranges: DateRange[]): DateRange[] {
const seen = new Set<string>();
@@ -3,7 +3,7 @@ import { describe, it, expect } from "vitest";
import dayjs from "@calcom/dayjs";
import type { DateRange } from "@calcom/lib/date-ranges";
import { mergeOverlappingDateRanges } from ".";
import { mergeOverlappingDateRanges } from "./mergeOverlappingDateRanges";
const november2 = "2023-11-02";
const november3 = "2023-11-03";
@@ -1,37 +0,0 @@
import type { DateRange } from "@calcom/lib/date-ranges";
import { intersect } from "@calcom/lib/date-ranges";
import { SchedulingType } from "@calcom/prisma/enums";
import { mergeOverlappingDateRanges } from "./date-range-utils/mergeOverlappingDateRanges";
export const getAggregatedAvailability = (
userAvailability: {
dateRanges: DateRange[];
oooExcludedDateRanges: DateRange[];
user?: { isFixed?: boolean };
}[],
schedulingType: SchedulingType | null
): DateRange[] => {
const isTeamEvent =
schedulingType === SchedulingType.COLLECTIVE ||
schedulingType === SchedulingType.ROUND_ROBIN ||
userAvailability.length > 1;
const fixedHosts = userAvailability.filter(
({ user }) => !schedulingType || schedulingType === SchedulingType.COLLECTIVE || user?.isFixed
);
const dateRangesToIntersect = fixedHosts.map((s) =>
!isTeamEvent ? s.dateRanges : s.oooExcludedDateRanges
);
const unfixedHosts = userAvailability.filter(({ user }) => user?.isFixed !== true);
if (unfixedHosts.length) {
dateRangesToIntersect.push(
unfixedHosts.flatMap((s) => (!isTeamEvent ? s.dateRanges : s.oooExcludedDateRanges))
);
}
const availability = intersect(dateRangesToIntersect);
return mergeOverlappingDateRanges(availability);
};
@@ -1,3 +0,0 @@
export { getAggregatedAvailability as getAggregatedAvailabilityNew } from "./getAggregatedAvailability";
export { getAggregatedAvailability as getAggregatedAvailability } from "./getAggregatedAvailabilityOld";