revert: Reapply "fix: Multiple rr-hosts combine to create erroneous availabil… (#19263)
* Reapply "fix: Multiple rr-hosts combine to create erroneous availability (#18772)"
This reverts commit 5dbc6d9ca6.
* Only thing left is a diff to show which slots are removed
* Add logging around time slot results
* Setup logging through axiom
* .... forgot
* fix: type errors
* Only log when there are differences
* Updated from old which had a failing test to new logic
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
+254
-129
@@ -31,18 +31,18 @@ describe("Tests the date-range slot logic", () => {
|
||||
frequency: 60,
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 60,
|
||||
organizerTimeZone: "Etc/GMT",
|
||||
dateRanges: dateRangesNextDay,
|
||||
})
|
||||
).toHaveLength(24);
|
||||
});
|
||||
|
||||
it("can fit 24 hourly slots for an empty day with interval != eventLength", async () => {
|
||||
expect(
|
||||
getSlots({
|
||||
inviteeDate: dayjs.utc().add(1, "day"),
|
||||
frequency: 60,
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 60,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
eventLength: 30,
|
||||
dateRanges: dateRangesNextDay,
|
||||
})
|
||||
).toHaveLength(24);
|
||||
@@ -59,7 +59,6 @@ describe("Tests the date-range slot logic", () => {
|
||||
dateRanges: dateRangesMockDay,
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
})
|
||||
).toHaveLength(12);
|
||||
});
|
||||
@@ -74,7 +73,6 @@ describe("Tests the date-range slot logic", () => {
|
||||
dateRanges: dateRangesNextDay,
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
})
|
||||
).toHaveLength(11);
|
||||
});
|
||||
@@ -88,10 +86,260 @@ describe("Tests the date-range slot logic", () => {
|
||||
dateRanges: dateRangesNextDay,
|
||||
eventLength: 20,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
});
|
||||
expect(result).toHaveLength(72);
|
||||
});
|
||||
|
||||
it("can create multiple time slot groups when multiple date ranges are given", async () => {
|
||||
const nextDay = dayjs.utc().add(1, "day").startOf("day");
|
||||
const dateRanges = [
|
||||
// 11:00-11:20,11:20-11:40,11:40-12:00
|
||||
{
|
||||
start: nextDay.hour(11),
|
||||
end: nextDay.hour(12),
|
||||
},
|
||||
// 14:00-14:20,14:20-14:40,14:40-15:00
|
||||
{
|
||||
start: nextDay.hour(14),
|
||||
end: nextDay.hour(15),
|
||||
},
|
||||
];
|
||||
const result = getSlots({
|
||||
inviteeDate: nextDay,
|
||||
frequency: 20,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRanges,
|
||||
eventLength: 20,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(6);
|
||||
});
|
||||
|
||||
it("can merge multiple time slot groups when multiple date ranges are given that overlap", async () => {
|
||||
const nextDay = dayjs.utc().add(1, "day").startOf("day");
|
||||
const dateRanges = [
|
||||
// 11:00-11:20,11:20-11:40,11:40-12:00
|
||||
{
|
||||
start: nextDay.hour(11),
|
||||
end: nextDay.hour(12),
|
||||
},
|
||||
// 12:00-12:20,12:20-12:40
|
||||
{
|
||||
start: nextDay.hour(11).minute(20),
|
||||
end: nextDay.hour(12).minute(40),
|
||||
},
|
||||
];
|
||||
const result = getSlots({
|
||||
inviteeDate: nextDay,
|
||||
frequency: 20,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRanges,
|
||||
eventLength: 20,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(5);
|
||||
});
|
||||
|
||||
// for now, stay consistent with current behaviour and enable the slot 11:00, 11:45
|
||||
// however, optimal slot allocation is 11:15-12:00,12:00-12:45 (as both hosts can be routed to at this time)
|
||||
it("finds correct slots when two unequal date ranges are given", async () => {
|
||||
const nextDay = dayjs.utc().add(1, "day").startOf("day");
|
||||
const dateRanges = [
|
||||
// 11:00-13:00
|
||||
{
|
||||
start: nextDay.hour(11),
|
||||
end: nextDay.hour(13),
|
||||
},
|
||||
// 11:15-13:00
|
||||
{
|
||||
start: nextDay.hour(11).minute(15),
|
||||
end: nextDay.hour(13),
|
||||
},
|
||||
];
|
||||
const result = getSlots({
|
||||
inviteeDate: nextDay,
|
||||
frequency: 45,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRanges,
|
||||
eventLength: 45,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
it("finds correct slots when two unequal date ranges are given (inverse)", async () => {
|
||||
const nextDay = dayjs.utc().add(1, "day").startOf("day");
|
||||
|
||||
const dateRangesInverseOrder = [
|
||||
// 11:15-13:00
|
||||
{
|
||||
start: nextDay.hour(11).minute(15),
|
||||
end: nextDay.hour(13),
|
||||
},
|
||||
// 11:00-13:00
|
||||
{
|
||||
start: nextDay.hour(11),
|
||||
end: nextDay.hour(13),
|
||||
},
|
||||
];
|
||||
|
||||
const resultInverseOrder = getSlots({
|
||||
inviteeDate: nextDay,
|
||||
frequency: 45,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRangesInverseOrder,
|
||||
eventLength: 45,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
expect(resultInverseOrder).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("finds correct slots over the span of multiple days", async () => {
|
||||
const inviteeDate = dayjs.utc().add(1, "day").startOf("day");
|
||||
|
||||
const dateRanges = [
|
||||
// 11:30-14:00
|
||||
{
|
||||
start: inviteeDate.hour(11).minute(30),
|
||||
end: inviteeDate.hour(14),
|
||||
},
|
||||
// 9:15-13:00
|
||||
{
|
||||
start: inviteeDate.hour(9).minute(15),
|
||||
end: inviteeDate.hour(11),
|
||||
},
|
||||
// 11:30-14:00
|
||||
{
|
||||
start: inviteeDate.add(1, "day").hour(11).minute(30),
|
||||
end: inviteeDate.add(1, "day").hour(14),
|
||||
},
|
||||
// 11:15-13:00
|
||||
{
|
||||
start: inviteeDate.add(1, "day").hour(9).minute(15),
|
||||
end: inviteeDate.add(1, "day").hour(11),
|
||||
},
|
||||
];
|
||||
|
||||
// each day availability should go 10:00, 12:00, 13:00
|
||||
|
||||
const result = getSlots({
|
||||
// right now from the perspective of invitee local time.
|
||||
inviteeDate,
|
||||
frequency: 60,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: dateRanges,
|
||||
eventLength: 60,
|
||||
offsetStart: 0,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(6);
|
||||
});
|
||||
|
||||
it("shows correct time slots for 20 minutes long events with working hours that do not end at a full hour", async () => {
|
||||
const result = getSlots({
|
||||
inviteeDate: dayjs().add(1, "day"),
|
||||
frequency: 20,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: [{ start: dayjs("2021-06-21T00:00:00.000Z"), end: dayjs("2021-06-21T23:45:00.000Z") }],
|
||||
/*workingHours: [
|
||||
{
|
||||
userId: 1,
|
||||
days: Array.from(Array(7).keys()),
|
||||
startTime: MINUTES_DAY_START,
|
||||
endTime: MINUTES_DAY_END - 14, // 23:45
|
||||
},
|
||||
],*/
|
||||
eventLength: 20,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
});
|
||||
|
||||
// 71 20-minutes events in a 24h - 15m day
|
||||
expect(result).toHaveLength(71);
|
||||
});
|
||||
|
||||
it("tests the final slot of the day is included", async () => {
|
||||
const slots = getSlots({
|
||||
inviteeDate: dayjs.tz("2023-07-13T00:00:00.000", "Europe/Brussels"),
|
||||
eventLength: 15,
|
||||
offsetStart: 0,
|
||||
dateRanges: [
|
||||
{ start: dayjs("2023-07-13T07:00:00.000Z"), end: dayjs("2023-07-13T15:00:00.000Z") },
|
||||
{ start: dayjs("2023-07-13T18:30:00.000Z"), end: dayjs("2023-07-13T20:59:59.000Z") },
|
||||
],
|
||||
minimumBookingNotice: 120,
|
||||
frequency: 15,
|
||||
organizerTimeZone: "Europe/London",
|
||||
}).reverse();
|
||||
|
||||
expect(slots[0].time.format()).toBe("2023-07-13T22:45:00+02:00");
|
||||
});
|
||||
|
||||
it("tests slots for half hour timezones", async () => {
|
||||
const slots = getSlots({
|
||||
inviteeDate: dayjs.tz("2023-07-13T00:00:00.000", "Asia/Kolkata"),
|
||||
frequency: 60,
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 60,
|
||||
dateRanges: [
|
||||
{
|
||||
start: dayjs.tz("2023-07-13T07:30:00.000", "Asia/Kolkata"),
|
||||
end: dayjs.tz("2023-07-13T09:30:00.000", "Asia/Kolkata"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(slots).toHaveLength(1);
|
||||
expect(slots[0].time.format()).toBe("2023-07-13T08:00:00+05:30");
|
||||
});
|
||||
|
||||
it("tests slots for 5 minute events", async () => {
|
||||
const slots = getSlots({
|
||||
inviteeDate: dayjs.tz("2023-07-13T00:00:00.000+05:30", "Europe/London"),
|
||||
frequency: 5,
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 5,
|
||||
dateRanges: [
|
||||
// fits 1 slot
|
||||
{
|
||||
start: dayjs.tz("2023-07-13T07:00:00.000", "Europe/London"),
|
||||
end: dayjs.tz("2023-07-13T07:05:00.000", "Europe/London"),
|
||||
},
|
||||
// fits 4 slots
|
||||
{
|
||||
start: dayjs.tz("2023-07-13T07:10:00.000", "Europe/London"),
|
||||
end: dayjs.tz("2023-07-13T07:30:00.000", "Europe/London"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(slots).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("tests slots for events with an event length that is not divisible by 5", async () => {
|
||||
const slots = getSlots({
|
||||
inviteeDate: dayjs.utc().startOf("day"),
|
||||
frequency: 8,
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 8,
|
||||
dateRanges: [
|
||||
{
|
||||
start: dayjs.utc("2023-07-13T07:22:00.000"),
|
||||
end: dayjs.utc("2023-07-13T08:00:00.000"),
|
||||
},
|
||||
],
|
||||
});
|
||||
/*
|
||||
* 2023-07-13T07:22:00.000Z
|
||||
* 2023-07-13T07:30:00.000Z
|
||||
* 2023-07-13T07:38:00.000Z
|
||||
* 2023-07-13T07:46:00.000Z
|
||||
*/
|
||||
expect(slots).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests the slot logic", () => {
|
||||
@@ -205,29 +453,6 @@ describe("Tests the slot logic", () => {
|
||||
).toHaveLength(11);
|
||||
});
|
||||
|
||||
it("shows correct time slots for 20 minutes long events with working hours that do not end at a full hour", async () => {
|
||||
const result = getSlots({
|
||||
inviteeDate: dayjs().add(1, "day"),
|
||||
frequency: 20,
|
||||
minimumBookingNotice: 0,
|
||||
dateRanges: [{ start: dayjs("2021-06-21T00:00:00.000Z"), end: dayjs("2021-06-21T23:45:00.000Z") }],
|
||||
/*workingHours: [
|
||||
{
|
||||
userId: 1,
|
||||
days: Array.from(Array(7).keys()),
|
||||
startTime: MINUTES_DAY_START,
|
||||
endTime: MINUTES_DAY_END - 14, // 23:45
|
||||
},
|
||||
],*/
|
||||
eventLength: 20,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
});
|
||||
|
||||
// 71 20-minutes events in a 24h - 15m day
|
||||
expect(result).toHaveLength(71);
|
||||
});
|
||||
|
||||
it("can fit 48 25 minute slots with a 5 minute offset for an empty day", async () => {
|
||||
expect(
|
||||
getSlots({
|
||||
@@ -248,105 +473,6 @@ describe("Tests the slot logic", () => {
|
||||
})
|
||||
).toHaveLength(48);
|
||||
});
|
||||
|
||||
it("tests the final slot of the day is included", async () => {
|
||||
const slots = getSlots({
|
||||
inviteeDate: dayjs.tz("2023-07-13T00:00:00.000+02:00", "Europe/Brussels"),
|
||||
eventLength: 15,
|
||||
workingHours: [
|
||||
{
|
||||
days: [1, 2, 3, 4, 5],
|
||||
startTime: 480,
|
||||
endTime: 960,
|
||||
userId: 9,
|
||||
},
|
||||
{
|
||||
days: [4],
|
||||
startTime: 1170,
|
||||
endTime: 1379,
|
||||
userId: 9,
|
||||
},
|
||||
],
|
||||
dateOverrides: [],
|
||||
offsetStart: 0,
|
||||
dateRanges: [
|
||||
{ start: dayjs("2023-07-13T07:00:00.000Z"), end: dayjs("2023-07-13T15:00:00.000Z") },
|
||||
{ start: dayjs("2023-07-13T18:30:00.000Z"), end: dayjs("2023-07-13T20:59:59.000Z") },
|
||||
],
|
||||
minimumBookingNotice: 120,
|
||||
frequency: 15,
|
||||
organizerTimeZone: "Europe/London",
|
||||
}).reverse();
|
||||
|
||||
expect(slots[0].time.format()).toBe("2023-07-13T22:45:00+02:00");
|
||||
});
|
||||
|
||||
it("tests slots for half hour timezones", async () => {
|
||||
const slots = getSlots({
|
||||
inviteeDate: dayjs.tz("2023-07-13T00:00:00.000+05:30", "Asia/Kolkata"),
|
||||
frequency: 60,
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 60,
|
||||
organizerTimeZone: "Asia/Kolkata",
|
||||
dateRanges: [
|
||||
{
|
||||
start: dayjs.tz("2023-07-13T07:30:00.000", "Asia/Kolkata"),
|
||||
end: dayjs.tz("2023-07-13T09:30:00.000", "Asia/Kolkata"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(slots).toHaveLength(1);
|
||||
expect(slots[0].time.format()).toBe("2023-07-13T08:00:00+05:30");
|
||||
});
|
||||
|
||||
it("tests slots for 5 minute events", async () => {
|
||||
const slots = getSlots({
|
||||
inviteeDate: dayjs.tz("2023-07-13T00:00:00.000+05:30", "Europe/London"),
|
||||
frequency: 5,
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 5,
|
||||
organizerTimeZone: "Europe/London",
|
||||
dateRanges: [
|
||||
// fits 1 slot
|
||||
{
|
||||
start: dayjs.tz("2023-07-13T07:00:00.000", "Europe/London"),
|
||||
end: dayjs.tz("2023-07-13T07:05:00.000", "Europe/London"),
|
||||
},
|
||||
// fits 4 slots
|
||||
{
|
||||
start: dayjs.tz("2023-07-13T07:10:00.000", "Europe/London"),
|
||||
end: dayjs.tz("2023-07-13T07:30:00.000", "Europe/London"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(slots).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("tests slots for events with an event length that is not divisible by 5", async () => {
|
||||
const slots = getSlots({
|
||||
inviteeDate: dayjs.tz("2023-07-13T00:00:00.000+05:30", "Europe/London"),
|
||||
frequency: 8,
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 8,
|
||||
organizerTimeZone: "Europe/London",
|
||||
dateRanges: [
|
||||
{
|
||||
start: dayjs.tz("2023-07-13T07:22:00.000", "Europe/London"),
|
||||
end: dayjs.tz("2023-07-13T08:00:00.000", "Europe/London"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/*
|
||||
2023-07-13T06:22:00.000Z
|
||||
2023-07-13T06:30:00.000Z
|
||||
2023-07-13T06:38:00.000Z
|
||||
2023-07-13T06:46:00.000Z
|
||||
*/
|
||||
expect(slots).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests the date-range slot logic with custom env variable", () => {
|
||||
@@ -385,7 +511,6 @@ describe("Tests the date-range slot logic with custom env variable", () => {
|
||||
minimumBookingNotice: 0,
|
||||
eventLength: 10,
|
||||
offsetStart: 0,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
dateRanges: [{ start: dayjs("2023-07-13T00:10:00.000Z"), end: dayjs("2023-07-13T02:00:00.000Z") }],
|
||||
})
|
||||
).toHaveLength(11);
|
||||
|
||||
+149
-6
@@ -16,7 +16,7 @@ export type GetSlots = {
|
||||
minimumBookingNotice: number;
|
||||
eventLength: number;
|
||||
offsetStart?: number;
|
||||
organizerTimeZone: string;
|
||||
organizerTimeZone?: string;
|
||||
datesOutOfOffice?: IOutOfOfficeData;
|
||||
};
|
||||
export type TimeFrame = { userIds?: number[]; startTime: number; endTime: number };
|
||||
@@ -141,7 +141,7 @@ function buildSlots({
|
||||
return slots;
|
||||
}
|
||||
|
||||
function buildSlotsWithDateRanges({
|
||||
function buildSlotsWithDateRangesOld({
|
||||
dateRanges,
|
||||
frequency,
|
||||
eventLength,
|
||||
@@ -241,6 +241,128 @@ function buildSlotsWithDateRanges({
|
||||
return slots;
|
||||
}
|
||||
|
||||
function buildSlotsWithDateRanges({
|
||||
dateRanges,
|
||||
frequency,
|
||||
eventLength,
|
||||
timeZone,
|
||||
minimumBookingNotice,
|
||||
offsetStart,
|
||||
datesOutOfOffice,
|
||||
}: {
|
||||
dateRanges: DateRange[];
|
||||
frequency: number;
|
||||
eventLength: number;
|
||||
timeZone: string;
|
||||
minimumBookingNotice: number;
|
||||
offsetStart?: number;
|
||||
datesOutOfOffice?: IOutOfOfficeData;
|
||||
}) {
|
||||
// keep the old safeguards in; may be needed.
|
||||
frequency = minimumOfOne(frequency);
|
||||
eventLength = minimumOfOne(eventLength);
|
||||
offsetStart = offsetStart ? minimumOfOne(offsetStart) : 0;
|
||||
// there can only ever be one slot at a given start time, and based on duration also only a single length.
|
||||
const slots = new Map<
|
||||
string,
|
||||
{
|
||||
time: Dayjs;
|
||||
userIds?: number[];
|
||||
away?: boolean;
|
||||
fromUser?: IFromUser;
|
||||
toUser?: IToUser;
|
||||
reason?: string;
|
||||
emoji?: string;
|
||||
}
|
||||
>();
|
||||
|
||||
let interval = Number(process.env.NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL) || 1;
|
||||
const intervalsWithDefinedStartTimes = [60, 30, 20, 15, 10, 5];
|
||||
|
||||
for (let i = 0; i < intervalsWithDefinedStartTimes.length; i++) {
|
||||
if (frequency % intervalsWithDefinedStartTimes[i] === 0) {
|
||||
interval = intervalsWithDefinedStartTimes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const startTimeWithMinNotice = dayjs.utc().add(minimumBookingNotice, "minute");
|
||||
|
||||
const orderedDateRanges = dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
|
||||
orderedDateRanges.forEach((range) => {
|
||||
const dateYYYYMMDD = range.start.format("YYYY-MM-DD");
|
||||
|
||||
let slotStartTime = range.start.utc().isAfter(startTimeWithMinNotice)
|
||||
? range.start
|
||||
: startTimeWithMinNotice;
|
||||
|
||||
slotStartTime =
|
||||
slotStartTime.minute() % interval !== 0
|
||||
? slotStartTime.startOf("hour").add(Math.ceil(slotStartTime.minute() / interval) * interval, "minute")
|
||||
: slotStartTime;
|
||||
|
||||
slotStartTime = slotStartTime.add(offsetStart ?? 0, "minutes").tz(timeZone);
|
||||
|
||||
// if the slotStartTime is between an existing slot, we need to adjust to the begin of the existing slot
|
||||
// but that adjusted startTime must be legal.
|
||||
const iterator = slots.keys();
|
||||
let result = iterator.next();
|
||||
|
||||
while (!result.done) {
|
||||
const utcResultValue = dayjs.utc(result.value);
|
||||
// if the slotStartTime is between an existing slot, we need to adjust to the begin of the existing slot
|
||||
if (
|
||||
utcResultValue.isBefore(slotStartTime) &&
|
||||
utcResultValue.add(frequency + (offsetStart ?? 0), "minutes").isAfter(slotStartTime)
|
||||
) {
|
||||
// however, the slot can now be before the start of this date range.
|
||||
if (!utcResultValue.isBefore(range.start)) {
|
||||
// it is between, if possible floor down to the start of the existing slot
|
||||
slotStartTime = utcResultValue;
|
||||
} else {
|
||||
// if not possible to floor, we need to ceil up to the next slot.
|
||||
slotStartTime = utcResultValue.add(frequency + (offsetStart ?? 0), "minutes");
|
||||
}
|
||||
// and then convert to the correct timezone - UTC mode is just for performance.
|
||||
slotStartTime = slotStartTime.tz(timeZone);
|
||||
}
|
||||
result = iterator.next();
|
||||
}
|
||||
while (!slotStartTime.add(eventLength, "minutes").subtract(1, "second").utc().isAfter(range.end)) {
|
||||
const dateOutOfOfficeExists = datesOutOfOffice?.[dateYYYYMMDD];
|
||||
let slotData: {
|
||||
time: Dayjs;
|
||||
userIds?: number[];
|
||||
away?: boolean;
|
||||
fromUser?: IFromUser;
|
||||
toUser?: IToUser;
|
||||
reason?: string;
|
||||
emoji?: string;
|
||||
} = {
|
||||
time: slotStartTime,
|
||||
};
|
||||
|
||||
if (dateOutOfOfficeExists) {
|
||||
const { toUser, fromUser, reason, emoji } = dateOutOfOfficeExists;
|
||||
|
||||
slotData = {
|
||||
time: slotStartTime,
|
||||
away: true,
|
||||
...(fromUser && { fromUser }),
|
||||
...(toUser && { toUser }),
|
||||
...(reason && { reason }),
|
||||
...(emoji && { emoji }),
|
||||
};
|
||||
}
|
||||
|
||||
slots.set(slotData.time.toISOString(), slotData);
|
||||
slotStartTime = slotStartTime.add(frequency + (offsetStart ?? 0), "minutes");
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(slots.values());
|
||||
}
|
||||
|
||||
function fromIndex<T>(cb: (val: T, i: number, a: T[]) => boolean, index: number) {
|
||||
return function (e: T, i: number, a: T[]) {
|
||||
return i >= index && cb(e, i, a);
|
||||
@@ -258,9 +380,27 @@ const getSlots = ({
|
||||
offsetStart = 0,
|
||||
organizerTimeZone,
|
||||
datesOutOfOffice,
|
||||
}: GetSlots) => {
|
||||
if (dateRanges) {
|
||||
const slots = buildSlotsWithDateRanges({
|
||||
}: GetSlots): {
|
||||
time: Dayjs;
|
||||
userIds?: number[];
|
||||
away?: boolean;
|
||||
fromUser?: IFromUser;
|
||||
toUser?: IToUser;
|
||||
reason?: string;
|
||||
emoji?: string;
|
||||
}[] => {
|
||||
if (dateRanges && !organizerTimeZone) {
|
||||
return buildSlotsWithDateRanges({
|
||||
dateRanges,
|
||||
frequency,
|
||||
eventLength,
|
||||
timeZone: getTimeZone(inviteeDate),
|
||||
minimumBookingNotice,
|
||||
offsetStart,
|
||||
datesOutOfOffice,
|
||||
});
|
||||
} else if (dateRanges && organizerTimeZone) {
|
||||
return buildSlotsWithDateRangesOld({
|
||||
dateRanges,
|
||||
frequency,
|
||||
eventLength,
|
||||
@@ -270,7 +410,10 @@ const getSlots = ({
|
||||
offsetStart,
|
||||
datesOutOfOffice,
|
||||
});
|
||||
return slots;
|
||||
}
|
||||
|
||||
if (!organizerTimeZone) {
|
||||
throw new Error("organizerTimeZone is required during getSlots call without dateRanges");
|
||||
}
|
||||
|
||||
// current date in invitee tz
|
||||
|
||||
@@ -3,7 +3,10 @@ import { countBy } from "lodash";
|
||||
import type { Logger } from "tslog";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { getAggregatedAvailability } from "@calcom/core/getAggregatedAvailability";
|
||||
import {
|
||||
getAggregatedAvailability,
|
||||
getAggregatedAvailabilityNew,
|
||||
} from "@calcom/core/getAggregatedAvailability";
|
||||
import { getBusyTimesForLimitChecks } from "@calcom/core/getBusyTimes";
|
||||
import type { CurrentSeats, GetAvailabilityUser, IFromUser, IToUser } from "@calcom/core/getUserAvailability";
|
||||
import { getUsersAvailability } from "@calcom/core/getUserAvailability";
|
||||
@@ -183,6 +186,20 @@ const selectSelectedSlots = Prisma.validator<Prisma.SelectedSlotsDefaultArgs>()(
|
||||
|
||||
type SelectedSlots = Prisma.SelectedSlotsGetPayload<typeof selectSelectedSlots>;
|
||||
|
||||
const groupTimeSlotsByDay = (timeSlots: { time: Dayjs }[]) => {
|
||||
return timeSlots.reduce((acc: Record<string, string[]>, { time }) => {
|
||||
const day = time.format("YYYY-MM-DD"); // Group by date
|
||||
const formattedTime = time.format("HH:mm"); // Format as HH:mm
|
||||
|
||||
if (!acc[day]) {
|
||||
acc[day] = [];
|
||||
}
|
||||
acc[day].push(formattedTime);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
function applyOccupiedSeatsToCurrentSeats(currentSeats: CurrentSeats, occupiedSeats: SelectedSlots[]) {
|
||||
const occupiedSeatsCount = countBy(occupiedSeats, (item) => item.slotUtcStartDate.toISOString());
|
||||
Object.keys(occupiedSeatsCount).forEach((date) => {
|
||||
@@ -310,6 +327,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
: dayjs(startTimeAsIsoString).subtract(1, "month").toISOString();
|
||||
|
||||
const loggerWithEventDetails = logger.getSubLogger({
|
||||
type: "json",
|
||||
prefix: ["getAvailableSlots", `${eventType.id}:${input.usernameList}/${input.eventTypeSlug}`],
|
||||
});
|
||||
|
||||
@@ -350,18 +368,19 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
contactOwnerEmail,
|
||||
});
|
||||
|
||||
let { aggregatedAvailability, allUsersAvailability, usersWithCredentials, currentSeats } =
|
||||
await calculateHostsAndAvailabilities({
|
||||
input,
|
||||
eventType,
|
||||
hosts: routedHostsWithContactOwnerAndFixedHosts,
|
||||
contactOwnerEmail,
|
||||
loggerWithEventDetails,
|
||||
startTime,
|
||||
endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
shouldServeCache,
|
||||
});
|
||||
let { allUsersAvailability, usersWithCredentials, currentSeats } = await calculateHostsAndAvailabilities({
|
||||
input,
|
||||
eventType,
|
||||
hosts: routedHostsWithContactOwnerAndFixedHosts,
|
||||
contactOwnerEmail,
|
||||
loggerWithEventDetails,
|
||||
startTime,
|
||||
endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
shouldServeCache,
|
||||
});
|
||||
|
||||
let aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType);
|
||||
|
||||
// If contact skipping, determine if there's availability within two weeks
|
||||
if (contactOwnerEmail && aggregatedAvailability.length > 0) {
|
||||
@@ -375,8 +394,8 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
|
||||
if (routedHostsAndFixedHosts.length > 0) {
|
||||
// if the first available slot is more than 2 weeks from now, round robin as normal
|
||||
({ aggregatedAvailability, allUsersAvailability, usersWithCredentials, currentSeats } =
|
||||
await calculateHostsAndAvailabilities({
|
||||
({ allUsersAvailability, usersWithCredentials, currentSeats } = await calculateHostsAndAvailabilities(
|
||||
{
|
||||
input,
|
||||
eventType,
|
||||
hosts: routedHostsAndFixedHosts,
|
||||
@@ -386,7 +405,9 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
shouldServeCache,
|
||||
}));
|
||||
}
|
||||
));
|
||||
aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -401,6 +422,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
// TODO: Also, handleNewBooking only seems to be using eventType?.schedule?.timeZone which seems to confirm that we should simplify it as well.
|
||||
const eventTimeZone =
|
||||
eventType.timeZone || eventType?.schedule?.timeZone || allUsersAvailability?.[0]?.timeZone;
|
||||
|
||||
const timeSlots = monitorCallbackSync(getSlots, {
|
||||
inviteeDate: startTime,
|
||||
eventLength: input.duration || eventType.length,
|
||||
@@ -412,6 +434,47 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
datesOutOfOffice: !isTeamEvent ? allUsersAvailability[0]?.datesOutOfOffice : undefined,
|
||||
});
|
||||
|
||||
const aggregatedAvailabilityNew = getAggregatedAvailabilityNew(
|
||||
allUsersAvailability,
|
||||
eventType.schedulingType
|
||||
);
|
||||
|
||||
const timeSlotsNew = monitorCallbackSync(getSlots, {
|
||||
inviteeDate: startTime,
|
||||
eventLength: input.duration || eventType.length,
|
||||
offsetStart: eventType.offsetStart,
|
||||
dateRanges: aggregatedAvailabilityNew,
|
||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||
frequency: eventType.slotInterval || input.duration || eventType.length,
|
||||
datesOutOfOffice: !isTeamEvent ? allUsersAvailability[0]?.datesOutOfOffice : undefined,
|
||||
});
|
||||
|
||||
const ts = groupTimeSlotsByDay(timeSlots),
|
||||
tsNew = groupTimeSlotsByDay(timeSlotsNew);
|
||||
|
||||
const differences = Object.keys({ ...ts, ...tsNew }).reduce(
|
||||
(acc: Record<string, { onlyInTimeSlots: string[]; onlyInTimeSlotsNew: string[] }>, day) => {
|
||||
const t1 = ts[day] || [],
|
||||
t2 = tsNew[day] || [];
|
||||
const missingInNew = t1.filter((t) => !t2.includes(t));
|
||||
const missingInOld = t2.filter((t) => !t1.includes(t));
|
||||
|
||||
if (missingInNew.length || missingInOld.length) {
|
||||
acc[day] = { onlyInTimeSlots: missingInNew, onlyInTimeSlotsNew: missingInOld };
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (Object.keys(differences).length) {
|
||||
loggerWithEventDetails.info({
|
||||
routedTeamMemberIds,
|
||||
differences,
|
||||
});
|
||||
}
|
||||
|
||||
let availableTimeSlots: typeof timeSlots = [];
|
||||
// Load cached busy slots
|
||||
const selectedSlots =
|
||||
@@ -548,11 +611,11 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
|
||||
);
|
||||
});
|
||||
|
||||
loggerWithEventDetails.debug(safeStringify({ slotsMappedToDate }));
|
||||
loggerWithEventDetails.debug({ slotsMappedToDate });
|
||||
|
||||
const availableDates = Object.keys(slotsMappedToDate);
|
||||
const allDatesWithBookabilityStatus = monitorCallbackSync(getAllDatesWithBookabilityStatus, availableDates);
|
||||
loggerWithEventDetails.debug(safeStringify({ availableDates }));
|
||||
loggerWithEventDetails.debug({ availableDates });
|
||||
|
||||
const eventUtcOffset = getUTCOffsetByTimezone(eventTimeZone) ?? 0;
|
||||
const bookerUtcOffset = input.timeZone ? getUTCOffsetByTimezone(input.timeZone) ?? 0 : 0;
|
||||
@@ -1024,14 +1087,7 @@ const calculateHostsAndAvailabilities = async ({
|
||||
}
|
||||
);
|
||||
|
||||
const aggregatedAvailability = monitorCallbackSync(
|
||||
getAggregatedAvailability,
|
||||
allUsersAvailability,
|
||||
eventType.schedulingType
|
||||
);
|
||||
|
||||
return {
|
||||
aggregatedAvailability,
|
||||
allUsersAvailability,
|
||||
usersWithCredentials,
|
||||
currentSeats,
|
||||
|
||||
Reference in New Issue
Block a user