Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

revert: Reapply "fix: Multiple rr-hosts combine to create erroneous availabil… #19263

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { describe, it, expect } from "vitest";

import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";

import { getAggregatedAvailabilityNew as getAggregatedAvailability } from ".";

// Helper to check if a time range overlaps with availability
const isAvailable = (availability: { start: Dayjs; end: Dayjs }[], range: { start: Dayjs; end: Dayjs }) => {
return availability.some(({ start, end }) => {
return start <= range.start && end >= range.end;
});
};

describe("getAggregatedAvailability", () => {
// rr-host availability used to combine into erroneous slots, this confirms it no longer happens
it("should not merge RR availability resulting in an unavailable slot due to overlap", () => {
const userAvailability = [
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T11:20:00.000Z") },
{ start: dayjs("2025-01-23T16:10:00.000Z"), end: dayjs("2025-01-23T16:30:00.000Z") },
],
user: { isFixed: false },
},
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T11:15:00.000Z"), end: dayjs("2025-01-23T11:30:00.000Z") },
{ start: dayjs("2025-01-23T13:20:00.000Z"), end: dayjs("2025-01-23T13:30:00.000Z") },
],
user: { isFixed: false },
},
];

const result = getAggregatedAvailability(userAvailability, "ROUND_ROBIN");
const timeRangeToCheckBusy = {
start: dayjs("2025-01-23T11:00:00.000Z"),
end: dayjs("2025-01-23T11:30:00.000Z"),
};

expect(isAvailable(result, timeRangeToCheckBusy)).toBe(false);

const timeRangeToCheckAvailable = {
start: dayjs("2025-01-23T11:00:00.000Z"),
end: dayjs("2025-01-23T11:20:00.000Z"),
};

expect(isAvailable(result, timeRangeToCheckAvailable)).toBe(true);
});

it("it returns the right amount of date ranges even if the end time is before the start time", () => {
const userAvailability = [
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-27T14:00:00.000Z"), end: dayjs("2025-01-27T04:30-05:00") },
],
user: { isFixed: false },
},
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-27T14:00:00.000Z"), end: dayjs("2025-01-27T14:45:00.000Z") },
],
user: { isFixed: false },
},
];

const result = getAggregatedAvailability(userAvailability, "ROUND_ROBIN");

expect(result).toEqual([
{
start: dayjs("2025-01-27T14:00:00.000Z"),
end: dayjs("2025-01-27T09:30:00.000Z"),
},
{
start: dayjs("2025-01-27T14:00:00.000Z"),
end: dayjs("2025-01-27T14:45:00.000Z"),
},
]);
});

// validates fixed host behaviour, they all have to be available
it("correctly joins fixed host availability resulting in one or more combined date ranges", () => {
const userAvailability = [
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T11:20:00.000Z") },
{ start: dayjs("2025-01-23T16:10:00.000Z"), end: dayjs("2025-01-23T16:30:00.000Z") },
],
user: { isFixed: true },
},
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T11:15:00.000Z"), end: dayjs("2025-01-23T11:30:00.000Z") },
{ start: dayjs("2025-01-23T13:20:00.000Z"), end: dayjs("2025-01-23T13:30:00.000Z") },
],
user: { isFixed: true },
},
];

const result = getAggregatedAvailability(userAvailability, "ROUND_ROBIN");
const timeRangeToCheckBusy = {
start: dayjs("2025-01-23T11:00:00.000Z"),
end: dayjs("2025-01-23T11:30:00.000Z"),
};

expect(isAvailable(result, timeRangeToCheckBusy)).toBe(false);

expect(result[0].start.format()).toEqual(dayjs("2025-01-23T11:15:00.000Z").format());
expect(result[0].end.format()).toEqual(dayjs("2025-01-23T11:20:00.000Z").format());
});

// Combines rr hosts and fixed hosts, both fixed and one of the rr hosts has to be available for the whole period
// All fixed user ranges are merged with each rr-host
it("Fixed hosts and at least one rr host available between 11:00-11:30 & 12:30-13:00 on January 23, 2025", () => {
// Both fixed user A and B are available 11:00-11:30 & 12:30-13:00 & 13:15-13:30
// Only user C (rr) is available 11:00-11:30 and only user D (rr) is available 12:30-13:00
// No rr users are available 13:15-13:30 and this date range should not be a result.
const userAvailability = [
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T11:30:00.000Z") },
{ start: dayjs("2025-01-23T12:30:00.000Z"), end: dayjs("2025-01-23T13:00:00.000Z") },
{ start: dayjs("2025-01-23T13:15:00.000Z"), end: dayjs("2025-01-23T13:30:00.000Z") },
{ start: dayjs("2025-01-23T16:10:00.000Z"), end: dayjs("2025-01-23T16:30:00.000Z") },
],
user: { isFixed: true },
},
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T11:30:00.000Z") },
{ start: dayjs("2025-01-23T12:30:00.000Z"), end: dayjs("2025-01-23T13:00:00.000Z") },
{ start: dayjs("2025-01-23T13:15:00.000Z"), end: dayjs("2025-01-23T13:30:00.000Z") },
{ start: dayjs("2025-01-23T13:20:00.000Z"), end: dayjs("2025-01-23T13:30:00.000Z") },
],
user: { isFixed: true },
},
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T11:30:00.000Z") },
],
user: { isFixed: false },
},
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T12:30:00.000Z"), end: dayjs("2025-01-23T13:00:00.000Z") },
],
user: { isFixed: false },
},
];

const result = getAggregatedAvailability(userAvailability, "ROUND_ROBIN");
const timeRangeToCheckAvailable = {
start: dayjs("2025-01-23T11:00:00.000Z"),
end: dayjs("2025-01-23T11:30:00.000Z"),
};

expect(isAvailable(result, timeRangeToCheckAvailable)).toBe(true);

expect(result[0].start.format()).toEqual(dayjs("2025-01-23T11:00:00.000Z").format());
expect(result[0].end.format()).toEqual(dayjs("2025-01-23T11:30:00.000Z").format());
expect(result[1].start.format()).toEqual(dayjs("2025-01-23T12:30:00.000Z").format());
expect(result[1].end.format()).toEqual(dayjs("2025-01-23T13:00:00.000Z").format());
});

it("does not duplicate slots when multiple rr-hosts offer the same availability", () => {
const userAvailability = [
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T11:30:00.000Z") },
{ start: dayjs("2025-01-23T12:30:00.000Z"), end: dayjs("2025-01-23T13:00:00.000Z") },
{ start: dayjs("2025-01-23T13:15:00.000Z"), end: dayjs("2025-01-23T13:30:00.000Z") },
{ start: dayjs("2025-01-23T16:10:00.000Z"), end: dayjs("2025-01-23T16:30:00.000Z") },
],
user: { isFixed: true },
},
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T11:30:00.000Z") },
],
user: { isFixed: false },
},
{
dateRanges: [],
oooExcludedDateRanges: [
{ start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T11:30:00.000Z") },
],
user: { isFixed: false },
},
];

const result = getAggregatedAvailability(userAvailability, "ROUND_ROBIN");
const timeRangeToCheckAvailable = {
start: dayjs("2025-01-23T11:00:00.000Z"),
end: dayjs("2025-01-23T11:30:00.000Z"),
};

expect(isAvailable(result, timeRangeToCheckAvailable)).toBe(true);
expect(result.length).toBe(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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";

function uniqueAndSortedDateRanges(ranges: DateRange[]): DateRange[] {
const seen = new Set<string>();

return ranges
.sort((a, b) => {
const startDiff = a.start.valueOf() - b.start.valueOf();
return startDiff !== 0 ? startDiff : a.end.valueOf() - b.end.valueOf();
})
.filter((range) => {
const key = `${range.start.valueOf()}-${range.end.valueOf()}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}

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 fixedDateRanges = mergeOverlappingDateRanges(
intersect(fixedHosts.map((s) => (!isTeamEvent ? s.dateRanges : s.oooExcludedDateRanges)))
);
const dateRangesToIntersect = !!fixedDateRanges.length ? [fixedDateRanges] : [];
const roundRobinHosts = userAvailability.filter(({ user }) => user?.isFixed !== true);
if (roundRobinHosts.length) {
dateRangesToIntersect.push(
roundRobinHosts.flatMap((s) => (!isTeamEvent ? s.dateRanges : s.oooExcludedDateRanges))
);
}
const availability = intersect(dateRangesToIntersect);
// we no longer merge overlapping date ranges, rr-hosts need to be individually available here.
return uniqueAndSortedDateRanges(availability);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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);
};
38 changes: 2 additions & 36 deletions packages/core/getAggregatedAvailability/index.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,3 @@
import type { DateRange } from "@calcom/lib/date-ranges";
import { intersect } from "@calcom/lib/date-ranges";
import { SchedulingType } from "@calcom/prisma/enums";
export { getAggregatedAvailability as getAggregatedAvailabilityNew } from "./getAggregatedAvailability";

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);
};
export { getAggregatedAvailability as getAggregatedAvailability } from "./getAggregatedAvailabilityOld";
Loading
Loading