* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main: - Rebrand Cal.com to Cal.diy across the entire codebase - Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions - Switch license from AGPL-3.0 to MIT - Remove docs/ directory (migrated to Nextra at cal.diy) - Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc. - Clean up .env.example for self-hosted Cal.diy - Update Docker image references to calcom/cal.diy - Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork - Add PR welcome bot for Cal.diy contributors - Fix API v2 breaking changes oasdiff ignore entries - Replace Blacksmith CI runners with default GitHub Actions 3893 files changed, 20789 insertions(+), 411020 deletions(-) Co-Authored-By: [email protected] <[email protected]> * refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rip out org related comments in api v2 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
3889 lines
114 KiB
TypeScript
3889 lines
114 KiB
TypeScript
import CalendarManagerMock from "@calcom/features/calendars/lib/__mocks__/CalendarManager";
|
|
import { constantsScenarios } from "@calcom/lib/__mocks__/constants";
|
|
|
|
import {
|
|
getDate,
|
|
getGoogleCalendarCredential,
|
|
createBookingScenario,
|
|
createOrganization,
|
|
getOrganizer,
|
|
getScenarioData,
|
|
Timezones,
|
|
TestData,
|
|
createCredentials,
|
|
mockCrmApp,
|
|
} from "@calcom/testing/lib/bookingScenario/bookingScenario";
|
|
|
|
import { describe, vi, test } from "vitest";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";
|
|
import { SchedulingType, type BookingStatus } from "@calcom/prisma/enums";
|
|
|
|
import { expect, expectedSlotsForSchedule } from "./getSchedule/expects";
|
|
import { setupAndTeardown } from "./getSchedule/setupAndTeardown";
|
|
import { timeTravelToTheBeginningOfToday } from "./getSchedule/utils";
|
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
constantsScenarios.set({
|
|
IS_PRODUCTION: true,
|
|
WEBAPP_URL: "http://localhost:3000",
|
|
RESERVED_SUBDOMAINS: ["auth", "docs"],
|
|
SINGLE_ORG_SLUG: "",
|
|
} as any);
|
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
|
|
describe("getSchedule", () => {
|
|
const availableSlotsService = getAvailableSlotsService();
|
|
setupAndTeardown();
|
|
|
|
// TODO: Move these inside describe('Team Event')
|
|
describe("Round robin lead skip(i.e. use contact owner specified by teamMemberEmail) - CRM", async () => {
|
|
test("correctly get slots for event with only round robin hosts", async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
|
|
const crmCredential = {
|
|
id: 1,
|
|
type: "salesforce_crm",
|
|
key: {
|
|
clientId: "test-client-id",
|
|
},
|
|
userId: 1,
|
|
teamId: null,
|
|
appId: "salesforce",
|
|
invalid: false,
|
|
user: { email: "[email protected]" },
|
|
};
|
|
|
|
await createCredentials([crmCredential]);
|
|
|
|
mockCrmApp("salesforce", {
|
|
getContacts: [
|
|
{
|
|
id: "contact-id",
|
|
email: "[email protected]",
|
|
ownerEmail: "[email protected]",
|
|
},
|
|
],
|
|
createContacts: [{ id: "contact-id", email: "[email protected]" }],
|
|
});
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
metadata: {
|
|
apps: {
|
|
salesforce: {
|
|
enabled: true,
|
|
appCategories: ["crm"],
|
|
roundRobinLeadSkip: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstEveningShift],
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// only slots where [email protected] is available
|
|
expect(scheduleWithLeadSkip).toHaveTimeSlots(
|
|
[`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
|
|
const scheduleWithoutLeadSkip = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// slots where either one of the rr hosts is available
|
|
expect(scheduleWithoutLeadSkip).toHaveTimeSlots(
|
|
[
|
|
`04:30:00.000Z`,
|
|
`05:30:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:30:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:30:00.000Z`,
|
|
`11:30:00.000Z`,
|
|
`12:30:00.000Z`,
|
|
`13:30:00.000Z`,
|
|
`14:30:00.000Z`,
|
|
`15:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
test("correctly get slots for event with round robin and fixed hosts", async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
|
|
const crmCredential = {
|
|
id: 1,
|
|
type: "salesforce_crm",
|
|
key: {
|
|
clientId: "test-client-id",
|
|
},
|
|
userId: 1,
|
|
teamId: null,
|
|
appId: "salesforce",
|
|
invalid: false,
|
|
user: { email: "[email protected]" },
|
|
};
|
|
|
|
await createCredentials([crmCredential]);
|
|
|
|
mockCrmApp("salesforce", {
|
|
getContacts: [
|
|
{
|
|
id: "contact-id",
|
|
email: "[email protected]",
|
|
ownerEmail: "[email protected]",
|
|
},
|
|
{
|
|
id: "contact-id-1",
|
|
email: "[email protected]",
|
|
ownerEmail: "[email protected]",
|
|
},
|
|
],
|
|
createContacts: [{ id: "contact-id", email: "[email protected]" }],
|
|
});
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: true,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 103,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
metadata: {
|
|
apps: {
|
|
salesforce: {
|
|
enabled: true,
|
|
appCategories: ["crm"],
|
|
roundRobinLeadSkip: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstMidShift],
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 103,
|
|
schedules: [TestData.schedules.IstEveningShift],
|
|
|
|
defaultScheduleId: 3,
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const scheduleFixedHostLead = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// show normal slots, example@example + one RR host needs to be available
|
|
expect(scheduleFixedHostLead).toHaveTimeSlots(
|
|
[
|
|
`07:30:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:30:00.000Z`,
|
|
`11:30:00.000Z`,
|
|
`12:30:00.000Z`,
|
|
`13:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
|
|
const scheduleRRHostLead = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// slots where example@example (fixed host) + [email protected] are available together
|
|
expect(scheduleRRHostLead).toHaveTimeSlots(
|
|
[`07:30:00.000Z`, `08:30:00.000Z`, `09:30:00.000Z`, `10:30:00.000Z`, `11:30:00.000Z`],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
test("correctly get slots when contact owner has no availability in first two weeks", async () => {
|
|
vi.setSystemTime("2024-05-01T00:00:13Z");
|
|
|
|
const startTime = "2024-05-01";
|
|
const endTime = "2024-06-01";
|
|
const dateWithinTwoWeeks = "2024-05-10";
|
|
const dateAfterTwoWeeks = "2024-05-22";
|
|
|
|
const crmCredential = {
|
|
id: 1,
|
|
type: "salesforce_crm",
|
|
key: {
|
|
clientId: "test-client-id",
|
|
},
|
|
userId: 1,
|
|
teamId: null,
|
|
appId: "salesforce",
|
|
invalid: false,
|
|
user: { email: "[email protected]" },
|
|
};
|
|
|
|
await createCredentials([crmCredential]);
|
|
|
|
mockCrmApp("salesforce", {
|
|
getContacts: [
|
|
{
|
|
id: "contact-id",
|
|
email: "[email protected]",
|
|
ownerEmail: "[email protected]",
|
|
},
|
|
],
|
|
createContacts: [{ id: "contact-id", email: "[email protected]" }],
|
|
});
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
metadata: {
|
|
apps: {
|
|
salesforce: {
|
|
enabled: true,
|
|
appCategories: ["crm"],
|
|
roundRobinLeadSkip: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHoursWithFirstTwoWeeksUnavailable(startTime)],
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${startTime}T18:30:00.000Z`,
|
|
endTime: `${endTime}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// [email protected] isn't available in the first two weeks, so we fallback to all hosts
|
|
expect(scheduleWithLeadSkip).toHaveTimeSlots(
|
|
[
|
|
`04:30:00.000Z`,
|
|
`05:30:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:30:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:30:00.000Z`,
|
|
`11:30:00.000Z`,
|
|
// `12:30:00.000Z`,
|
|
// `13:30:00.000Z`,
|
|
// `14:30:00.000Z`,
|
|
// `15:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: dateWithinTwoWeeks,
|
|
}
|
|
);
|
|
|
|
// slots from from both hosts because [email protected] isn't available in the first two weeks, so we fallback to all hosts
|
|
expect(scheduleWithLeadSkip).toHaveTimeSlots(
|
|
[
|
|
`04:30:00.000Z`,
|
|
`05:30:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:30:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:30:00.000Z`,
|
|
`11:30:00.000Z`,
|
|
`12:30:00.000Z`,
|
|
`13:30:00.000Z`,
|
|
`14:30:00.000Z`,
|
|
`15:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: dateAfterTwoWeeks,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("correctly get slots for event when contact owner has no availability for the whole month but is available the first two weeks", async () => {
|
|
vi.setSystemTime("2024-05-01T00:00:13Z");
|
|
|
|
const startTime = "2024-07-01";
|
|
const endTime = "2024-07-31";
|
|
|
|
const crmCredential = {
|
|
id: 1,
|
|
type: "salesforce_crm",
|
|
key: {
|
|
clientId: "test-client-id",
|
|
},
|
|
userId: 1,
|
|
teamId: null,
|
|
appId: "salesforce",
|
|
invalid: false,
|
|
user: { email: "[email protected]" },
|
|
};
|
|
|
|
await createCredentials([crmCredential]);
|
|
|
|
mockCrmApp("salesforce", {
|
|
getContacts: [
|
|
{
|
|
id: "contact-id",
|
|
email: "[email protected]",
|
|
ownerEmail: "[email protected]",
|
|
},
|
|
],
|
|
createContacts: [{ id: "contact-id", email: "[email protected]" }],
|
|
});
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
metadata: {
|
|
apps: {
|
|
salesforce: {
|
|
enabled: true,
|
|
appCategories: ["crm"],
|
|
roundRobinLeadSkip: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstNotAvailableForFullMonth("2024-07")], // not available full July
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${startTime}T18:30:00.000Z`,
|
|
endTime: `${endTime}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// slots from [email protected] because even though user isn't available in July, user is available in the first two weeks
|
|
expect(scheduleWithLeadSkip).toHaveDateDisabled({
|
|
dateString: "2024-07-05",
|
|
});
|
|
});
|
|
|
|
test("correctly get slots for event when contact owner has no availability at all", async () => {
|
|
vi.setSystemTime("2024-05-01T00:00:13Z");
|
|
|
|
const startTime = "2024-07-01";
|
|
const endTime = "2024-07-31";
|
|
|
|
const crmCredential = {
|
|
id: 1,
|
|
type: "salesforce_crm",
|
|
key: {
|
|
clientId: "test-client-id",
|
|
},
|
|
userId: 1,
|
|
teamId: null,
|
|
appId: "salesforce",
|
|
invalid: false,
|
|
user: { email: "[email protected]" },
|
|
};
|
|
|
|
await createCredentials([crmCredential]);
|
|
|
|
mockCrmApp("salesforce", {
|
|
getContacts: [
|
|
{
|
|
id: "contact-id",
|
|
email: "[email protected]",
|
|
ownerEmail: "[email protected]",
|
|
},
|
|
],
|
|
createContacts: [{ id: "contact-id", email: "[email protected]" }],
|
|
});
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
metadata: {
|
|
apps: {
|
|
salesforce: {
|
|
enabled: true,
|
|
appCategories: ["crm"],
|
|
roundRobinLeadSkip: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.EmptyAvailability],
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${startTime}T18:30:00.000Z`,
|
|
endTime: `${endTime}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// slots from [email protected] because [email protected] isn't available also not the first two weeks
|
|
expect(scheduleWithLeadSkip).toHaveTimeSlots(
|
|
[
|
|
`04:30:00.000Z`,
|
|
`05:30:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:30:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:30:00.000Z`,
|
|
`11:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: "2024-07-05",
|
|
}
|
|
);
|
|
});
|
|
|
|
test("correctly gets slot for event when contact owner is available in the first two weeks and the next month is loaded (within the two weeks)", async () => {
|
|
vi.setSystemTime("2024-05-23T00:00:13Z");
|
|
|
|
const startTime = "2024-06-01";
|
|
const endTime = "2024-06-31";
|
|
|
|
const crmCredential = {
|
|
id: 1,
|
|
type: "salesforce_crm",
|
|
key: {
|
|
clientId: "test-client-id",
|
|
},
|
|
userId: 1,
|
|
teamId: null,
|
|
appId: "salesforce",
|
|
invalid: false,
|
|
user: { email: "[email protected]" },
|
|
};
|
|
|
|
await createCredentials([crmCredential]);
|
|
|
|
mockCrmApp("salesforce", {
|
|
getContacts: [
|
|
{
|
|
id: "contact-id",
|
|
email: "[email protected]",
|
|
ownerEmail: "[email protected]",
|
|
},
|
|
],
|
|
createContacts: [{ id: "contact-id", email: "[email protected]" }],
|
|
});
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
metadata: {
|
|
apps: {
|
|
salesforce: {
|
|
enabled: true,
|
|
appCategories: ["crm"],
|
|
roundRobinLeadSkip: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstNotAvailableForFullMonth("2024-06")],
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${startTime}T18:30:00.000Z`,
|
|
endTime: `${endTime}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// slots for [email protected], because user is available in the first two weeks (prev month)
|
|
expect(scheduleWithLeadSkip).toHaveDateDisabled({
|
|
dateString: "2024-06-03",
|
|
});
|
|
});
|
|
|
|
test("correctly gets slot for event when contact owner is available in the first two weeks and it's end of the month", async () => {
|
|
vi.setSystemTime("2024-05-23T00:00:13Z");
|
|
|
|
const startTime = "2024-05-23";
|
|
const endTime = "2024-05-31";
|
|
|
|
const crmCredential = {
|
|
id: 1,
|
|
type: "salesforce_crm",
|
|
key: {
|
|
clientId: "test-client-id",
|
|
},
|
|
userId: 1,
|
|
teamId: null,
|
|
appId: "salesforce",
|
|
invalid: false,
|
|
user: { email: "[email protected]" },
|
|
};
|
|
|
|
await createCredentials([crmCredential]);
|
|
|
|
mockCrmApp("salesforce", {
|
|
getContacts: [
|
|
{
|
|
id: "contact-id",
|
|
email: "[email protected]",
|
|
ownerEmail: "[email protected]",
|
|
},
|
|
],
|
|
createContacts: [{ id: "contact-id", email: "[email protected]" }],
|
|
});
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
metadata: {
|
|
apps: {
|
|
salesforce: {
|
|
enabled: true,
|
|
appCategories: ["crm"],
|
|
roundRobinLeadSkip: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstNotAvailableForFullMonth("2024-05")],
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${startTime}T18:30:00.000Z`,
|
|
endTime: `${endTime}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// slots for [email protected], because user is available in the first two weeks (next month)
|
|
expect(scheduleWithLeadSkip).toHaveDateDisabled({
|
|
dateString: "2024-05-29",
|
|
});
|
|
});
|
|
|
|
test("Negative case(i.e. skipContactOwner is true) - when teamMemberEmail is provided but not used", async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
|
|
const crmCredential = {
|
|
id: 1,
|
|
type: "salesforce_crm",
|
|
key: {
|
|
clientId: "test-client-id",
|
|
},
|
|
userId: 1,
|
|
teamId: null,
|
|
appId: "salesforce",
|
|
invalid: false,
|
|
user: { email: "[email protected]" },
|
|
};
|
|
|
|
await createCredentials([crmCredential]);
|
|
|
|
mockCrmApp("salesforce", {
|
|
getContacts: [
|
|
{
|
|
id: "contact-id",
|
|
email: "[email protected]",
|
|
ownerEmail: "[email protected]",
|
|
},
|
|
],
|
|
createContacts: [{ id: "contact-id", email: "[email protected]" }],
|
|
});
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
metadata: {
|
|
apps: {
|
|
salesforce: {
|
|
enabled: true,
|
|
appCategories: ["crm"],
|
|
roundRobinLeadSkip: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstEveningShift],
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const scheduleWhenContactOwnerIsSkipped = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
skipContactOwner: true,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// Both users slot would be available as contact owner([email protected]) is skipped and we fallback to all users of RR
|
|
expect(scheduleWhenContactOwnerIsSkipped).toHaveTimeSlots(
|
|
[
|
|
`04:30:00.000Z`,
|
|
`05:30:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:30:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:30:00.000Z`,
|
|
`11:30:00.000Z`,
|
|
`12:30:00.000Z`,
|
|
`13:30:00.000Z`,
|
|
`14:30:00.000Z`,
|
|
`15:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("User Event", () => {
|
|
test("correctly identifies unavailable slots from Cal Bookings in different status", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
// An event with one accepted booking
|
|
await createBookingScenario({
|
|
// An event with length 30 minutes, slotInterval 45 minutes, and minimumBookingNotice 1440 minutes (24 hours)
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
// If `slotInterval` is set, it supersedes `length`
|
|
slotInterval: 45,
|
|
length: 45,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
},
|
|
],
|
|
bookings: [
|
|
// That event has one accepted booking from 4:00 to 4:15 in GMT on Day + 3 which is 9:30 to 9:45 in IST
|
|
{
|
|
eventTypeId: 1,
|
|
userId: 101,
|
|
status: "ACCEPTED",
|
|
// Booking Time is stored in GMT in DB. So, provide entry in GMT only.
|
|
startTime: `${plus3DateString}T04:00:00.000Z`,
|
|
endTime: `${plus3DateString}T04:15:00.000Z`,
|
|
},
|
|
{
|
|
eventTypeId: 1,
|
|
userId: 101,
|
|
status: "REJECTED",
|
|
// Booking Time is stored in GMT in DB. So, provide entry in GMT only.
|
|
startTime: `${plus2DateString}T04:00:00.000Z`,
|
|
endTime: `${plus2DateString}T04:15:00.000Z`,
|
|
},
|
|
{
|
|
eventTypeId: 1,
|
|
userId: 101,
|
|
status: "CANCELLED",
|
|
// Booking Time is stored in GMT in DB. So, provide entry in GMT only.
|
|
startTime: `${plus2DateString}T05:00:00.000Z`,
|
|
endTime: `${plus2DateString}T05:15:00.000Z`,
|
|
},
|
|
{
|
|
eventTypeId: 1,
|
|
userId: 101,
|
|
status: "PENDING",
|
|
// Booking Time is stored in GMT in DB. So, provide entry in GMT only.
|
|
startTime: `${plus2DateString}T06:00:00.000Z`,
|
|
endTime: `${plus2DateString}T06:15:00.000Z`,
|
|
},
|
|
],
|
|
});
|
|
|
|
// Day Plus 2 is completely free - It only has non accepted bookings
|
|
const scheduleOnCompletelyFreeDay = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
// EventTypeSlug doesn't matter for non-dynamic events
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// getSchedule returns timeslots in GMT
|
|
expect(scheduleOnCompletelyFreeDay).toHaveTimeSlots(
|
|
[
|
|
"04:00:00.000Z",
|
|
"04:45:00.000Z",
|
|
"05:30:00.000Z",
|
|
"06:15:00.000Z",
|
|
"07:00:00.000Z",
|
|
"07:45:00.000Z",
|
|
"08:30:00.000Z",
|
|
"09:15:00.000Z",
|
|
"10:00:00.000Z",
|
|
"10:45:00.000Z",
|
|
"11:30:00.000Z",
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
|
|
// Day plus 3
|
|
const scheduleForDayWithOneBooking = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus2DateString}T18:30:00.000Z`,
|
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
|
|
[
|
|
// "04:00:00.000Z", - This slot is unavailable because of the booking from 4:00 to 4:15
|
|
`04:15:00.000Z`,
|
|
`05:00:00.000Z`,
|
|
`05:45:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:15:00.000Z`,
|
|
`08:00:00.000Z`,
|
|
`08:45:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:15:00.000Z`,
|
|
`11:00:00.000Z`,
|
|
`11:45:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus3DateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("slots are available as per `length`, `slotInterval` of the event", async () => {
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 30,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
length: 30,
|
|
slotInterval: 120,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
},
|
|
],
|
|
});
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const scheduleForEventWith30Length = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
orgSlug: null,
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
},
|
|
});
|
|
|
|
expect(scheduleForEventWith30Length).toHaveTimeSlots(
|
|
[
|
|
`04:00:00.000Z`,
|
|
`04:30:00.000Z`,
|
|
`05:00:00.000Z`,
|
|
`05:30:00.000Z`,
|
|
`06:00:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:00:00.000Z`,
|
|
`07:30:00.000Z`,
|
|
`08:00:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`09:00:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:00:00.000Z`,
|
|
`10:30:00.000Z`,
|
|
`11:00:00.000Z`,
|
|
`11:30:00.000Z`,
|
|
`12:00:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
|
|
const scheduleForEventWith30minsLengthAndSlotInterval2hrs =
|
|
await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
orgSlug: null,
|
|
eventTypeId: 2,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
},
|
|
});
|
|
// `slotInterval` takes precedence over `length`
|
|
// 4:30 is utc so it is 10:00 in IST
|
|
expect(scheduleForEventWith30minsLengthAndSlotInterval2hrs).toHaveTimeSlots(
|
|
[`04:30:00.000Z`, `06:30:00.000Z`, `08:30:00.000Z`, `10:30:00.000Z`, `12:30:00.000Z`],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("minimumBookingNotice is respected", async () => {
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 2 * 60,
|
|
minimumBookingNotice: 13 * 60, // Would take the minimum bookable time to be 18:30UTC+13 = 7:30AM UTC
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
length: 2 * 60,
|
|
minimumBookingNotice: 10 * 60, // Would take the minimum bookable time to be 18:30UTC+10 = 4:30AM UTC
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
},
|
|
],
|
|
});
|
|
|
|
const { dateString: todayDateString } = getDate();
|
|
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
|
|
|
|
// Time Travel to the beginning of today after getting all the dates correctly.
|
|
timeTravelToTheBeginningOfToday({ utcOffsetInHours: 5.5 });
|
|
|
|
const scheduleForEventWithBookingNotice13Hrs = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${minus1DateString}T18:30:00.000Z`,
|
|
endTime: `${todayDateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
|
|
[
|
|
/*`03:30:00.000Z`, `05:30:00.000Z`, - Minimum time slot is 07:30 UTC which is 13hrs from 18:30*/
|
|
`07:30:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: todayDateString,
|
|
}
|
|
);
|
|
|
|
const scheduleForEventWithBookingNotice10Hrs = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 2,
|
|
eventTypeSlug: "",
|
|
startTime: `${minus1DateString}T18:30:00.000Z`,
|
|
endTime: `${todayDateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
|
|
[
|
|
/*`03:30:00.000Z`, - Minimum bookable time slot is 04:30 UTC which is 10hrs from 18:30 */
|
|
`04:30:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`10:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: todayDateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("afterBuffer and beforeBuffer tests - Non Cal Busy Time", async () => {
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({
|
|
success: true,
|
|
data: [
|
|
{
|
|
start: `${plus3DateString}T04:00:00.000Z`,
|
|
end: `${plus3DateString}T05:59:59.000Z`,
|
|
},
|
|
],
|
|
});
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 120,
|
|
beforeEventBuffer: 120,
|
|
afterEventBuffer: 120,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
credentials: [getGoogleCalendarCredential()],
|
|
selectedCalendars: [TestData.selectedCalendars.google],
|
|
},
|
|
],
|
|
apps: [TestData.apps["google-calendar"]],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const scheduleForEventOnADayWithNonCalBooking = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus2DateString}T18:30:00.000Z`,
|
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
|
|
[
|
|
// `05:30:00.000Z`, // - 5:30 AM is not available because 08:30AM slot has a `beforeEventBuffer`
|
|
`08:30:00.000Z`, // - 8:30 AM is available (2 PM IST)
|
|
`10:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus3DateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("afterBuffer and beforeBuffer tests - Cal Busy Time", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({
|
|
success: true,
|
|
data: [
|
|
{
|
|
start: `${plus3DateString}T04:00:00.000Z`,
|
|
end: `${plus3DateString}T05:59:59.000Z`,
|
|
},
|
|
],
|
|
});
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 120,
|
|
beforeEventBuffer: 120,
|
|
afterEventBuffer: 120,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
credentials: [getGoogleCalendarCredential()],
|
|
selectedCalendars: [TestData.selectedCalendars.google],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T04:00:00.000Z`,
|
|
endTime: `${plus2DateString}T05:59:59.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
apps: [TestData.apps["google-calendar"]],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const scheduleForEventOnADayWithCalBooking = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
|
|
[
|
|
// `05:30:00.000Z`, // - 5:30 AM is not available because of afterBuffer(120 mins) of the existing booking(4:00-5:59AM slot)
|
|
// `08:30:00.000Z`, // - 8:30 AM is not available because of beforeBuffer(120mins) of possible booking at 08:30
|
|
`10:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("Start times are offset (offsetStart)", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
|
|
CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue({ success: true, data: [] });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 25,
|
|
offsetStart: 5,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
credentials: [getGoogleCalendarCredential()],
|
|
selectedCalendars: [TestData.selectedCalendars.google],
|
|
},
|
|
],
|
|
apps: [TestData.apps["google-calendar"]],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const schedule = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(schedule).toHaveTimeSlots(
|
|
[
|
|
`04:05:00.000Z`,
|
|
`04:35:00.000Z`,
|
|
`05:05:00.000Z`,
|
|
`05:35:00.000Z`,
|
|
`06:05:00.000Z`,
|
|
`06:35:00.000Z`,
|
|
`07:05:00.000Z`,
|
|
`07:35:00.000Z`,
|
|
`08:05:00.000Z`,
|
|
`08:35:00.000Z`,
|
|
`09:05:00.000Z`,
|
|
`09:35:00.000Z`,
|
|
`10:05:00.000Z`,
|
|
`10:35:00.000Z`,
|
|
`11:05:00.000Z`,
|
|
`11:35:00.000Z`,
|
|
`12:05:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("Check for Date overrides", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHoursWithDateOverride(plus2DateString)],
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const scheduleForEventOnADayWithDateOverride = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
|
|
["08:30:00.000Z", "09:30:00.000Z", "10:30:00.000Z", "11:30:00.000Z"],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("that a user is considered busy when there's a booking they host", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
// A Collective Event Type hosted by this user
|
|
{
|
|
id: 1,
|
|
slotInterval: 45,
|
|
schedulingType: SchedulingType.COLLECTIVE,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
},
|
|
{
|
|
userId: 102,
|
|
},
|
|
],
|
|
},
|
|
// A default Event Type which this user owns
|
|
{
|
|
id: 2,
|
|
length: 15,
|
|
slotInterval: 45,
|
|
users: [{ id: 101 }],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
},
|
|
],
|
|
bookings: [
|
|
// Create a booking on our Collective Event Type
|
|
{
|
|
userId: 101,
|
|
attendees: [
|
|
{
|
|
email: "[email protected]",
|
|
},
|
|
],
|
|
eventTypeId: 1,
|
|
status: "ACCEPTED",
|
|
startTime: `${plus2DateString}T04:00:00.000Z`,
|
|
endTime: `${plus2DateString}T04:15:00.000Z`,
|
|
},
|
|
],
|
|
});
|
|
|
|
// Requesting this user's availability for their
|
|
// individual Event Type
|
|
const thisUserAvailability = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 2,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(thisUserAvailability).toHaveTimeSlots(
|
|
[
|
|
// `04:00:00.000Z`, // <- This slot should be occupied by the Collective Event
|
|
`04:15:00.000Z`,
|
|
`05:00:00.000Z`,
|
|
`05:45:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:15:00.000Z`,
|
|
`08:00:00.000Z`,
|
|
`08:45:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:15:00.000Z`,
|
|
`11:00:00.000Z`,
|
|
`11:45:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("test that booking limit is working correctly if user is all day available", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
bookingLimits: {
|
|
PER_DAY: 1,
|
|
},
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
bookingLimits: {
|
|
PER_DAY: 2,
|
|
},
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
id: 1,
|
|
name: "All Day available",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+6:00"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
// One bookings for each(E1 and E2) on plus2Date
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T08:00:00.000Z`,
|
|
endTime: `${plus2DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 2,
|
|
startTime: `${plus2DateString}T08:00:00.000Z`,
|
|
endTime: `${plus2DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const thisUserAvailabilityBookingLimitOne = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const thisUserAvailabilityBookingLimitTwo = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 2,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
let availableSlotsInTz: dayjs.Dayjs[] = [];
|
|
for (const date in thisUserAvailabilityBookingLimitOne.slots) {
|
|
thisUserAvailabilityBookingLimitOne.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit
|
|
|
|
availableSlotsInTz = [];
|
|
for (const date in thisUserAvailabilityBookingLimitTwo.slots) {
|
|
thisUserAvailabilityBookingLimitTwo.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(23); // 2 booking per day as limit, only one booking on that
|
|
});
|
|
|
|
test("test that duration limit is working correctly if user is all day available", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
durationLimits: {
|
|
PER_DAY: 120, // 2 hours per day
|
|
},
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
durationLimits: {
|
|
PER_DAY: 240, // 4 hours per day
|
|
},
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
id: 1,
|
|
name: "All Day available",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+6:00"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T08:00:00.000Z`,
|
|
endTime: `${plus2DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T10:00:00.000Z`,
|
|
endTime: `${plus2DateString}T11:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 2,
|
|
startTime: `${plus2DateString}T12:00:00.000Z`,
|
|
endTime: `${plus2DateString}T13:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const thisUserAvailabilityDurationLimitReached = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const thisUserAvailabilityDurationLimitNotReached = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 2,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
let availableSlotsInTz: dayjs.Dayjs[] = [];
|
|
for (const date in thisUserAvailabilityDurationLimitReached.slots) {
|
|
thisUserAvailabilityDurationLimitReached.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0);
|
|
|
|
availableSlotsInTz = [];
|
|
for (const date in thisUserAvailabilityDurationLimitNotReached.slots) {
|
|
thisUserAvailabilityDurationLimitNotReached.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(
|
|
availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length
|
|
).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("test that duration limit is working correctly with different timezone", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
durationLimits: {
|
|
PER_DAY: 120, // 2 hours per day
|
|
},
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
id: 1,
|
|
name: "All Day available",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+6:00"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T08:00:00.000Z`,
|
|
endTime: `${plus2DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T10:00:00.000Z`,
|
|
endTime: `${plus2DateString}T11:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const thisUserAvailabilityDurationLimit = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["-11:00"], // attendee timezone
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const availableSlotsInTz: dayjs.Dayjs[] = [];
|
|
for (const date in thisUserAvailabilityDurationLimit.slots) {
|
|
thisUserAvailabilityDurationLimit.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0);
|
|
});
|
|
|
|
test.skip("test that PER_WEEK duration limits work correctly", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
const { dateString: plus4DateString } = getDate({ dateIncrement: 4 });
|
|
const { dateString: plus5DateString } = getDate({ dateIncrement: 5 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
durationLimits: {
|
|
PER_WEEK: 180, // 3 hours per week
|
|
},
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
id: 1,
|
|
name: "All Day available",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+6:00"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus1DateString}T08:00:00.000Z`,
|
|
endTime: `${plus1DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T10:00:00.000Z`,
|
|
endTime: `${plus2DateString}T11:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus3DateString}T12:00:00.000Z`,
|
|
endTime: `${plus3DateString}T13:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const weeklyAvailability = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus5DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const availableSlotsInTz: dayjs.Dayjs[] = [];
|
|
for (const date in weeklyAvailability.slots) {
|
|
weeklyAvailability.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(
|
|
availableSlotsInTz.filter(
|
|
(slot) =>
|
|
slot.format().startsWith(plus1DateString) ||
|
|
slot.format().startsWith(plus2DateString) ||
|
|
slot.format().startsWith(plus3DateString) ||
|
|
slot.format().startsWith(plus4DateString)
|
|
).length
|
|
).toBe(0);
|
|
});
|
|
|
|
test.skip("global team duration limit blocks slots if one fixed host reached limit", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
team: {
|
|
id: 1,
|
|
durationLimits: { PER_DAY: 120 }, // 2 hours per day
|
|
},
|
|
schedulingType: SchedulingType.COLLECTIVE,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
{
|
|
id: 102,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
team: {
|
|
id: 1,
|
|
durationLimits: { PER_DAY: 120 }, // 2 hours per day
|
|
},
|
|
schedulingType: SchedulingType.COLLECTIVE,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
{
|
|
id: 102,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 3,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
id: 1,
|
|
name: "All Day available",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+6:00"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T08:00:00.000Z`,
|
|
endTime: `${plus2DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T10:00:00.000Z`,
|
|
endTime: `${plus2DateString}T11:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const availabilityEventTypeOne = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const availableSlotsInTz: dayjs.Dayjs[] = [];
|
|
for (const date in availabilityEventTypeOne.slots) {
|
|
availabilityEventTypeOne.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0);
|
|
|
|
const availabilityEventTypeTwo = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 2,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const availableSlotsInTz2: dayjs.Dayjs[] = [];
|
|
for (const date in availabilityEventTypeTwo.slots) {
|
|
availabilityEventTypeTwo.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz2.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz2.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0);
|
|
|
|
const availabilityUserEventType = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 3,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const availableSlotsInTz3: dayjs.Dayjs[] = [];
|
|
for (const date in availabilityUserEventType.slots) {
|
|
availabilityUserEventType.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz3.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(
|
|
availableSlotsInTz3.filter((slot) => slot.format().startsWith(plus2DateString)).length
|
|
).toBeGreaterThan(0);
|
|
});
|
|
|
|
test.skip("test that combined booking and duration limits work correctly", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
bookingLimits: {
|
|
PER_DAY: 3, // 3 bookings per day
|
|
},
|
|
durationLimits: {
|
|
PER_DAY: 120, // 2 hours per day
|
|
},
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
length: 30, // 30 minute event
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
bookingLimits: {
|
|
PER_DAY: 2, // 2 bookings per day
|
|
},
|
|
durationLimits: {
|
|
PER_DAY: 120, // 2 hours per day
|
|
},
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
id: 1,
|
|
name: "All Day available",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+6:00"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T08:00:00.000Z`,
|
|
endTime: `${plus2DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T10:00:00.000Z`,
|
|
endTime: `${plus2DateString}T11:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 2,
|
|
startTime: `${plus2DateString}T12:00:00.000Z`,
|
|
endTime: `${plus2DateString}T12:30:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const eventType1Availability = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const eventType2Availability = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 2,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
let availableSlotsInTz: dayjs.Dayjs[] = [];
|
|
for (const date in eventType1Availability.slots) {
|
|
eventType1Availability.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0);
|
|
|
|
availableSlotsInTz = [];
|
|
for (const date in eventType2Availability.slots) {
|
|
eventType2Availability.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(
|
|
availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length
|
|
).toBeGreaterThan(0);
|
|
|
|
// Create a new booking scenario with an additional booking
|
|
await createBookingScenario({
|
|
eventTypes: scenarioData.eventTypes,
|
|
users: scenarioData.users,
|
|
bookings: [
|
|
...scenarioData.bookings,
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 2,
|
|
startTime: `${plus2DateString}T14:00:00.000Z`,
|
|
endTime: `${plus2DateString}T14:30:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
});
|
|
|
|
const eventType2AvailabilityUpdated = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 2,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
availableSlotsInTz = [];
|
|
for (const date in eventType2AvailabilityUpdated.slots) {
|
|
eventType2AvailabilityUpdated.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0);
|
|
});
|
|
|
|
test("test that booking limit is working correctly if user is all day available and attendee is in different timezone than host", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
bookingLimits: {
|
|
PER_DAY: 1,
|
|
},
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
id: 1,
|
|
name: "All Day available",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+6:00"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
// One bookings for each(E1 and E2) on plus2Date
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T08:00:00.000Z`,
|
|
endTime: `${plus2DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const thisUserAvailabilityBookingLimit = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["-11:00"], //attendee timezone
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const availableSlotsInTz: dayjs.Dayjs[] = [];
|
|
for (const date in thisUserAvailabilityBookingLimit.slots) {
|
|
thisUserAvailabilityBookingLimit.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit
|
|
});
|
|
|
|
test("global team booking limit block slot if one fixed host reached limit", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
team: {
|
|
id: 1,
|
|
bookingLimits: { PER_DAY: 1 },
|
|
},
|
|
schedulingType: SchedulingType.COLLECTIVE,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
{
|
|
id: 102,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
team: {
|
|
id: 1,
|
|
bookingLimits: { PER_DAY: 1 },
|
|
},
|
|
schedulingType: SchedulingType.COLLECTIVE,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
{
|
|
id: 102,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 3,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
id: 1,
|
|
name: "All Day available",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+6:00"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T08:00:00.000Z`,
|
|
endTime: `${plus2DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const availabilityEventTypeOne = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const availableSlotsInTz: dayjs.Dayjs[] = [];
|
|
for (const date in availabilityEventTypeOne.slots) {
|
|
availabilityEventTypeOne.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit
|
|
|
|
const availabilityEventTypeTwo = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 2,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
for (const date in availabilityEventTypeTwo.slots) {
|
|
availabilityEventTypeTwo.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit
|
|
|
|
const availabilityUserEventType = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 3,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+6:00"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
for (const date in availabilityUserEventType.slots) {
|
|
availabilityUserEventType.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(23);
|
|
});
|
|
|
|
test("global team booking limit blocks correct slots if attendee and host are in different timezone", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
team: {
|
|
id: 1,
|
|
bookingLimits: { PER_DAY: 1 },
|
|
},
|
|
schedulingType: SchedulingType.COLLECTIVE,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
{
|
|
id: 102,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
id: 1,
|
|
name: "All Day available",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+6:00"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T08:00:00.000Z`,
|
|
endTime: `${plus2DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const thisUserAvailabilityBookingLimit = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["-11:00"], // attendee timezone
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const availableSlotsInTz: dayjs.Dayjs[] = [];
|
|
for (const date in thisUserAvailabilityBookingLimit.slots) {
|
|
thisUserAvailabilityBookingLimit.slots[date].forEach((timeObj) => {
|
|
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
|
|
});
|
|
}
|
|
|
|
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit
|
|
});
|
|
|
|
test("booking limits use schedule timezone when set, falling back to user timezone", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
const scenarioData = {
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
length: 60,
|
|
beforeEventBuffer: 0,
|
|
afterEventBuffer: 0,
|
|
bookingLimits: {
|
|
PER_DAY: 1,
|
|
},
|
|
schedule: {
|
|
id: 1,
|
|
name: "Schedule with timezone",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+5:30"],
|
|
},
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
id: 2,
|
|
name: "User schedule",
|
|
availability: [
|
|
{
|
|
userId: null,
|
|
eventTypeId: null,
|
|
days: [0, 1, 2, 3, 4, 5, 6],
|
|
startTime: new Date("1970-01-01T00:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T23:59:59.999Z"),
|
|
date: null,
|
|
},
|
|
],
|
|
timeZone: Timezones["+6:00"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
startTime: `${plus2DateString}T08:00:00.000Z`,
|
|
endTime: `${plus2DateString}T09:00:00.000Z`,
|
|
status: "ACCEPTED" as BookingStatus,
|
|
},
|
|
],
|
|
};
|
|
|
|
await createBookingScenario(scenarioData);
|
|
|
|
const availability = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T00:00:00.000Z`,
|
|
endTime: `${plus3DateString}T23:59:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
const availableSlotsInScheduleTz: dayjs.Dayjs[] = [];
|
|
for (const date in availability.slots) {
|
|
availability.slots[date].forEach((timeObj) => {
|
|
availableSlotsInScheduleTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"]));
|
|
});
|
|
}
|
|
|
|
expect(
|
|
availableSlotsInScheduleTz.filter((slot) => slot.format().startsWith(plus2DateString)).length
|
|
).toBe(0);
|
|
});
|
|
|
|
test("a slot counts as being busy when the eventType is requiresConfirmation and requiresConfirmationWillBlockSlot", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
// A default Event Type which this user owns
|
|
{
|
|
id: 2,
|
|
length: 15,
|
|
slotInterval: 45,
|
|
users: [{ id: 101 }],
|
|
requiresConfirmation: true,
|
|
requiresConfirmationWillBlockSlot: true,
|
|
},
|
|
{
|
|
id: 3,
|
|
length: 15,
|
|
slotInterval: 45,
|
|
users: [{ id: 101 }],
|
|
requiresConfirmation: true,
|
|
requiresConfirmationWillBlockSlot: false,
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
attendees: [
|
|
{
|
|
email: "[email protected]",
|
|
},
|
|
],
|
|
eventTypeId: 2,
|
|
status: "PENDING",
|
|
startTime: `${plus2DateString}T04:00:00.000Z`,
|
|
endTime: `${plus2DateString}T04:15:00.000Z`,
|
|
},
|
|
{
|
|
userId: 101,
|
|
attendees: [
|
|
{
|
|
email: "[email protected]",
|
|
},
|
|
],
|
|
eventTypeId: 3,
|
|
status: "PENDING",
|
|
startTime: `${plus2DateString}T05:00:00.000Z`,
|
|
endTime: `${plus2DateString}T05:15:00.000Z`,
|
|
},
|
|
],
|
|
});
|
|
|
|
// Requesting this user's availability for their
|
|
// individual Event Type
|
|
const thisUserAvailability = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 2,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(thisUserAvailability).toHaveTimeSlots(
|
|
[
|
|
// `04:00:00.000Z`, // <- This slot should be occupied by the Pending Requires Confirmation booking blocking this slot
|
|
`04:15:00.000Z`,
|
|
`05:00:00.000Z`, // <- This slot should be available to book as this event type (id: 3) has requires confirmation without blocking the slot
|
|
`05:45:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:15:00.000Z`,
|
|
`08:00:00.000Z`,
|
|
`08:45:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:15:00.000Z`,
|
|
`11:00:00.000Z`,
|
|
`11:45:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("Edge Case: Same day OOO entries that could accidentally be considered in past shouldn't crash the getSchedule", async () => {
|
|
// Set a fixed date for testing
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: todayDateString } = getDate({ dateIncrement: 0 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 45,
|
|
length: 45,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
outOfOffice: {
|
|
dateRanges: [
|
|
{
|
|
start: `${todayDateString}T00:00:00.000Z`,
|
|
end: `${todayDateString}T23:59:59.999Z`,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
const schedule = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// Verify that we get slots for the requested dates
|
|
expect(schedule).toHaveTimeSlots(
|
|
[
|
|
"04:00:00.000Z",
|
|
"04:45:00.000Z",
|
|
"05:30:00.000Z",
|
|
"06:15:00.000Z",
|
|
"07:00:00.000Z",
|
|
"07:45:00.000Z",
|
|
"08:30:00.000Z",
|
|
"09:15:00.000Z",
|
|
"10:00:00.000Z",
|
|
"10:45:00.000Z",
|
|
"11:30:00.000Z",
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Team Event", () => {
|
|
test("correctly identifies unavailable slots from calendar for all users in collective scheduling, considers bookings of users in other events as well", async () => {
|
|
const { dateString: todayDateString } = getDate();
|
|
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
// An event having two users with one accepted booking
|
|
{
|
|
id: 1,
|
|
slotInterval: 45,
|
|
schedulingType: SchedulingType.COLLECTIVE,
|
|
length: 45,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
{
|
|
id: 102,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
slotInterval: 45,
|
|
length: 45,
|
|
users: [
|
|
{
|
|
id: 102,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
status: "ACCEPTED",
|
|
startTime: `${plus2DateString}T04:00:00.000Z`,
|
|
endTime: `${plus2DateString}T04:15:00.000Z`,
|
|
},
|
|
{
|
|
userId: 102,
|
|
eventTypeId: 2,
|
|
status: "ACCEPTED",
|
|
startTime: `${plus2DateString}T05:30:00.000Z`,
|
|
endTime: `${plus2DateString}T05:45:00.000Z`,
|
|
},
|
|
],
|
|
});
|
|
|
|
const scheduleForTeamEventOnADayWithNoBooking = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${todayDateString}T18:30:00.000Z`,
|
|
endTime: `${plus1DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
|
[
|
|
`04:00:00.000Z`,
|
|
`04:45:00.000Z`,
|
|
`05:30:00.000Z`,
|
|
`06:15:00.000Z`,
|
|
`07:00:00.000Z`,
|
|
`07:45:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`09:15:00.000Z`,
|
|
`10:00:00.000Z`,
|
|
`10:45:00.000Z`,
|
|
`11:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus1DateString,
|
|
}
|
|
);
|
|
|
|
const scheduleForTeamEventOnADayWithOneBookingForEachUser =
|
|
await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
// A user with blocked time in another event, still affects Team Event availability
|
|
// It's a collective availability, so both user 101 and 102 are considered for timeslots
|
|
expect(scheduleForTeamEventOnADayWithOneBookingForEachUser).toHaveTimeSlots(
|
|
[
|
|
//`04:00:00.000Z`, - Blocked with User 101
|
|
`04:15:00.000Z`,
|
|
//`05:00:00.000Z`, - Blocked with User 102 in event 2
|
|
`05:45:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:15:00.000Z`,
|
|
`08:00:00.000Z`,
|
|
`08:45:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:15:00.000Z`,
|
|
`11:00:00.000Z`,
|
|
`11:45:00.000Z`,
|
|
],
|
|
{ dateString: plus2DateString }
|
|
);
|
|
});
|
|
|
|
test("correctly identifies unavailable slots from calendar for all users in Round Robin scheduling, considers bookings of users in other events as well", async () => {
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 });
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
// An event having two users with one accepted booking
|
|
{
|
|
id: 1,
|
|
slotInterval: 45,
|
|
length: 45,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
{
|
|
id: 102,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
},
|
|
{
|
|
id: 2,
|
|
slotInterval: 45,
|
|
length: 45,
|
|
users: [
|
|
{
|
|
id: 102,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
status: "ACCEPTED",
|
|
startTime: `${plus2DateString}T04:00:00.000Z`,
|
|
endTime: `${plus2DateString}T04:15:00.000Z`,
|
|
},
|
|
{
|
|
userId: 102,
|
|
eventTypeId: 2,
|
|
status: "ACCEPTED",
|
|
startTime: `${plus2DateString}T05:30:00.000Z`,
|
|
endTime: `${plus2DateString}T05:45:00.000Z`,
|
|
},
|
|
{
|
|
userId: 101,
|
|
eventTypeId: 1,
|
|
status: "ACCEPTED",
|
|
startTime: `${plus3DateString}T04:00:00.000Z`,
|
|
endTime: `${plus3DateString}T04:15:00.000Z`,
|
|
},
|
|
{
|
|
userId: 102,
|
|
eventTypeId: 2,
|
|
status: "ACCEPTED",
|
|
startTime: `${plus3DateString}T04:00:00.000Z`,
|
|
endTime: `${plus3DateString}T04:15:00.000Z`,
|
|
},
|
|
],
|
|
});
|
|
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots =
|
|
await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
// A user with blocked time in another event, still affects Team Event availability
|
|
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
|
|
[
|
|
`04:15:00.000Z`,
|
|
`05:45:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:15:00.000Z`,
|
|
`08:00:00.000Z`,
|
|
`08:45:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:15:00.000Z`,
|
|
`11:00:00.000Z`,
|
|
`11:45:00.000Z`,
|
|
],
|
|
{ dateString: plus2DateString }
|
|
);
|
|
|
|
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot =
|
|
await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus2DateString}T18:30:00.000Z`,
|
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
// A user with blocked time in another event, still affects Team Event availability
|
|
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
|
|
[
|
|
//`04:00:00.000Z`, // - Blocked with User 101 as well as User 102, so not available in Round Robin
|
|
`04:15:00.000Z`,
|
|
`05:00:00.000Z`,
|
|
`05:45:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:15:00.000Z`,
|
|
`08:00:00.000Z`,
|
|
`08:45:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:15:00.000Z`,
|
|
`11:00:00.000Z`,
|
|
`11:45:00.000Z`,
|
|
],
|
|
{ dateString: plus3DateString }
|
|
);
|
|
});
|
|
|
|
test("getSchedule can get slots of org's member event type when orgSlug, eventTypeSlug passed as input", async () => {
|
|
const org = await createOrganization({ name: "acme", slug: "acme" });
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
id: 101,
|
|
// So, that it picks the first schedule from the list
|
|
defaultScheduleId: null,
|
|
organizationId: org.id,
|
|
// Has morning shift with some overlap with morning shift
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
});
|
|
|
|
const scenario = await createBookingScenario(
|
|
getScenarioData(
|
|
{
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 45,
|
|
length: 45,
|
|
userId: 101,
|
|
},
|
|
],
|
|
organizer,
|
|
},
|
|
{ id: org.id }
|
|
)
|
|
);
|
|
|
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
|
|
|
const getScheduleRes = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeSlug: scenario.eventTypes[0]?.slug,
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: "acme",
|
|
usernameList: [organizer.username],
|
|
},
|
|
});
|
|
|
|
expect(getScheduleRes).toHaveTimeSlots(
|
|
[
|
|
`04:00:00.000Z`,
|
|
`04:45:00.000Z`,
|
|
`05:30:00.000Z`,
|
|
`06:15:00.000Z`,
|
|
`07:00:00.000Z`,
|
|
`07:45:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`09:15:00.000Z`,
|
|
`10:00:00.000Z`,
|
|
`10:45:00.000Z`,
|
|
`11:30:00.000Z`,
|
|
],
|
|
{ dateString: plus2DateString }
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Round robin event", async () => {
|
|
test("correctly get slots for event with round robin hosts chosen schedule (non-default)", async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
|
|
// An event with no common schedule and hosts chosen schedule (non-default schedule)
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
scheduleId: 2,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
scheduleId: 3,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
schedule: null,
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstMorningShift, TestData.schedules.IstEveningShift],
|
|
defaultScheduleId: 1,
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstEveningShift, TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 4,
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const scheduleWithHostChoosenSch = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
},
|
|
});
|
|
|
|
// expect only slots of IstEveningShift , as this is the chosen schedule for hosts
|
|
expect(scheduleWithHostChoosenSch).toHaveTimeSlots(
|
|
[`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
test("correctly get slots for event with common schedule", async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
|
|
// An event with common schedule
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedule: TestData.schedules.IstEveningShift,
|
|
schedulingType: "ROUND_ROBIN",
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 1,
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMidShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const scheduleWithEventCommonSch = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
teamMemberEmail: "[email protected]",
|
|
},
|
|
});
|
|
|
|
// expect only slots of IstEveningShift , as this is the common schedule of event
|
|
expect(scheduleWithEventCommonSch).toHaveTimeSlots(
|
|
[`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("Reschedule: should show timeslots of the same host if rescheduleWithSameRoundRobinHost is true", async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
|
|
// An event with common schedule
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstEveningShift],
|
|
defaultScheduleId: 1,
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
uid: "BOOKING_TO_RESCHEDULE_UID",
|
|
userId: 101,
|
|
attendees: [
|
|
{
|
|
email: "[email protected]",
|
|
},
|
|
],
|
|
eventTypeId: 1,
|
|
status: "ACCEPTED",
|
|
startTime: `${plus2DateString}T04:00:00.000Z`,
|
|
endTime: `${plus2DateString}T04:15:00.000Z`,
|
|
},
|
|
],
|
|
});
|
|
|
|
const schedule = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
rescheduleUid: "BOOKING_TO_RESCHEDULE_UID",
|
|
},
|
|
});
|
|
|
|
// With EE removal, rescheduleWithSameRoundRobinHost no longer filters to same host - all hosts' slots shown
|
|
expect(schedule).toHaveTimeSlots(
|
|
[
|
|
`04:30:00.000Z`,
|
|
`05:30:00.000Z`,
|
|
`06:30:00.000Z`,
|
|
`07:30:00.000Z`,
|
|
`08:30:00.000Z`,
|
|
`09:30:00.000Z`,
|
|
`10:30:00.000Z`,
|
|
`11:30:00.000Z`,
|
|
`12:30:00.000Z`,
|
|
`13:30:00.000Z`,
|
|
`14:30:00.000Z`,
|
|
`15:30:00.000Z`,
|
|
],
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
|
|
test("Reschedule: should show timeslots as per routedTeamMemberIds(instead of same host) even if rescheduleWithSameRoundRobinHost is true but it is a rerouting scenario",async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
|
|
// An event with common schedule
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
hosts: [
|
|
{
|
|
userId: 101,
|
|
isFixed: false,
|
|
},
|
|
{
|
|
userId: 102,
|
|
isFixed: false,
|
|
},
|
|
],
|
|
schedulingType: "ROUND_ROBIN",
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstEveningShift],
|
|
defaultScheduleId: 1,
|
|
},
|
|
{
|
|
...TestData.users.example,
|
|
email: "[email protected]",
|
|
id: 102,
|
|
schedules: [TestData.schedules.IstMorningShift],
|
|
defaultScheduleId: 2,
|
|
},
|
|
],
|
|
bookings: [
|
|
{
|
|
uid: "BOOKING_TO_RESCHEDULE_UID",
|
|
userId: 101,
|
|
attendees: [
|
|
{
|
|
email: "[email protected]",
|
|
},
|
|
],
|
|
eventTypeId: 1,
|
|
status: "ACCEPTED",
|
|
startTime: `${plus2DateString}T04:00:00.000Z`,
|
|
endTime: `${plus2DateString}T04:15:00.000Z`,
|
|
},
|
|
],
|
|
});
|
|
|
|
const schedule = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: true,
|
|
rescheduleUid: "BOOKING_TO_RESCHEDULE_UID",
|
|
routedTeamMemberIds: [102],
|
|
},
|
|
});
|
|
|
|
// expect only slots of IstEveningShift as this is the slots for the original host of the booking
|
|
expect(schedule).toHaveTimeSlots(
|
|
expectedSlotsForSchedule.IstMorningShift.interval["1hr"].allPossibleSlotsStartingAt430,
|
|
{
|
|
dateString: plus2DateString,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Empty working hours - early return optimization", () => {
|
|
test("returns no slots when workingHours is empty", async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [TestData.schedules.EmptyAvailability],
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const schedule = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(schedule).toHaveDateDisabled({
|
|
dateString: plus2DateString,
|
|
});
|
|
});
|
|
|
|
test("returns slots only for date override when workingHours is empty but date override exists", async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
name: "Empty schedule with date override",
|
|
availability: [
|
|
{
|
|
days: [],
|
|
startTime: new Date("1970-01-01T09:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T12:00:00.000Z"),
|
|
date: plus2DateString,
|
|
},
|
|
],
|
|
timeZone: Timezones["+5:30"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const schedule = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(schedule).toHaveTimeSlots([`03:30:00.000Z`, `04:30:00.000Z`, `05:30:00.000Z`], {
|
|
dateString: plus2DateString,
|
|
});
|
|
});
|
|
|
|
test("returns slots only for date overrides when workingHours is empty but multiple date overrides exist", async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
const plus3DateString = "2024-05-24";
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
name: "Empty schedule with multiple date overrides",
|
|
availability: [
|
|
{
|
|
days: [],
|
|
startTime: new Date("1970-01-01T09:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T11:00:00.000Z"),
|
|
date: plus2DateString,
|
|
},
|
|
{
|
|
days: [],
|
|
startTime: new Date("1970-01-01T14:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T16:00:00.000Z"),
|
|
date: plus3DateString,
|
|
},
|
|
],
|
|
timeZone: Timezones["+5:30"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const schedule = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(schedule).toHaveTimeSlots([`03:30:00.000Z`, `04:30:00.000Z`], {
|
|
dateString: plus2DateString,
|
|
});
|
|
|
|
expect(schedule).toHaveTimeSlots([`08:30:00.000Z`, `09:30:00.000Z`], {
|
|
dateString: plus3DateString,
|
|
});
|
|
});
|
|
|
|
test("returns no slots for dates without date override when workingHours is empty", async () => {
|
|
vi.setSystemTime("2024-05-21T00:00:13Z");
|
|
|
|
const plus1DateString = "2024-05-22";
|
|
const plus2DateString = "2024-05-23";
|
|
const plus3DateString = "2024-05-24";
|
|
|
|
await createBookingScenario({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 60,
|
|
length: 60,
|
|
users: [
|
|
{
|
|
id: 101,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
users: [
|
|
{
|
|
...TestData.users.example,
|
|
id: 101,
|
|
schedules: [
|
|
{
|
|
name: "Empty schedule with date override only on plus3",
|
|
availability: [
|
|
{
|
|
days: [],
|
|
startTime: new Date("1970-01-01T09:00:00.000Z"),
|
|
endTime: new Date("1970-01-01T12:00:00.000Z"),
|
|
date: plus3DateString,
|
|
},
|
|
],
|
|
timeZone: Timezones["+5:30"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
bookings: [],
|
|
});
|
|
|
|
const schedule = await availableSlotsService.getAvailableSlots({
|
|
input: {
|
|
eventTypeId: 1,
|
|
eventTypeSlug: "",
|
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
|
timeZone: Timezones["+5:30"],
|
|
isTeamEvent: false,
|
|
orgSlug: null,
|
|
},
|
|
});
|
|
|
|
expect(schedule).toHaveDateDisabled({
|
|
dateString: plus2DateString,
|
|
});
|
|
|
|
expect(schedule).toHaveTimeSlots([`03:30:00.000Z`, `04:30:00.000Z`, `05:30:00.000Z`], {
|
|
dateString: plus3DateString,
|
|
});
|
|
});
|
|
});
|
|
});
|