mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
1036 lines
38 KiB
TypeScript
1036 lines
38 KiB
TypeScript
import DatabaseService from "./DatabaseService";
|
|
import OnCallDutyPolicyScheduleLayerService from "./OnCallDutyPolicyScheduleLayerService";
|
|
import OnCallDutyPolicyScheduleLayerUserService from "./OnCallDutyPolicyScheduleLayerUserService";
|
|
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
import CalendarEvent from "../../Types/Calendar/CalendarEvent";
|
|
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
import OneUptimeDate from "../../Types/Date";
|
|
import ObjectID from "../../Types/ObjectID";
|
|
import LayerUtil, { LayerProps } from "../../Types/OnCallDutyPolicy/Layer";
|
|
import OnCallDutyPolicyScheduleLayer from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleLayer";
|
|
import OnCallDutyPolicyScheduleLayerUser from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser";
|
|
import User from "../../Models/DatabaseModels/User";
|
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
import OnCallDutyPolicySchedule from "../../Models/DatabaseModels/OnCallDutyPolicySchedule";
|
|
import OnCallDutyPolicyEscalationRuleSchedule from "../../Models/DatabaseModels/OnCallDutyPolicyEscalationRuleSchedule";
|
|
import OnCallDutyPolicyEscalationRuleScheduleService from "./OnCallDutyPolicyEscalationRuleScheduleService";
|
|
import Dictionary from "../../Types/Dictionary";
|
|
import { EmailEnvelope } from "../../Types/Email/EmailMessage";
|
|
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
|
import OnCallDutyPolicy from "../../Models/DatabaseModels/OnCallDutyPolicy";
|
|
import OnCallDutyPolicyEscalationRule from "../../Models/DatabaseModels/OnCallDutyPolicyEscalationRule";
|
|
import UserService from "./UserService";
|
|
import OnCallDutyPolicyService from "./OnCallDutyPolicyService";
|
|
import { SMSMessage } from "../../Types/SMS/SMS";
|
|
import { CallRequestMessage } from "../../Types/Call/CallRequest";
|
|
import UserNotificationSettingService from "./UserNotificationSettingService";
|
|
import NotificationSettingEventType from "../../Types/NotificationSetting/NotificationSettingEventType";
|
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
import Timezone from "../../Types/Timezone";
|
|
import logger from "../Utils/Logger";
|
|
import OnCallDutyPolicyFeedService from "./OnCallDutyPolicyFeedService";
|
|
import { OnCallDutyPolicyFeedEventType } from "../../Models/DatabaseModels/OnCallDutyPolicyFeed";
|
|
import { Green500 } from "../../Types/BrandColors";
|
|
import OnCallDutyPolicyTimeLogService from "./OnCallDutyPolicyTimeLogService";
|
|
import DeleteBy from "../Types/Database/DeleteBy";
|
|
import { OnDelete } from "../Types/Database/Hooks";
|
|
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
|
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
|
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
|
|
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
|
|
|
export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
private layerUtil = new LayerUtil();
|
|
|
|
public constructor() {
|
|
super(OnCallDutyPolicySchedule);
|
|
}
|
|
|
|
protected override async onBeforeDelete(
|
|
deleteBy: DeleteBy<OnCallDutyPolicySchedule>,
|
|
): Promise<OnDelete<OnCallDutyPolicySchedule>> {
|
|
const callSchedules: Array<OnCallDutyPolicySchedule> = await this.findBy({
|
|
query: deleteBy.query,
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
for (const schedule of callSchedules) {
|
|
OnCallDutyPolicyTimeLogService.endTimeForSchedule({
|
|
projectId: schedule.projectId!,
|
|
onCallDutyPolicyScheduleId: schedule.id!,
|
|
endsAt: OneUptimeDate.getCurrentDate(),
|
|
}).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
}
|
|
|
|
return {
|
|
deleteBy: deleteBy,
|
|
carryForward: {},
|
|
};
|
|
}
|
|
|
|
public async getOnCallSchedulesWhereUserIsOnCallDuty(data: {
|
|
projectId: ObjectID;
|
|
userId: ObjectID;
|
|
}): Promise<Array<OnCallDutyPolicySchedule>> {
|
|
const schedules: Array<OnCallDutyPolicySchedule> = await this.findBy({
|
|
query: {
|
|
projectId: data.projectId,
|
|
currentUserIdOnRoster: data.userId,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
name: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return schedules;
|
|
}
|
|
|
|
private async sendNotificationToUserOnScheduleHandoff(data: {
|
|
scheduleId: ObjectID;
|
|
previousInformation: {
|
|
currentUserIdOnRoster: ObjectID | null;
|
|
rosterHandoffAt: Date | null;
|
|
nextUserIdOnRoster: ObjectID | null;
|
|
nextHandOffTimeAt: Date | null;
|
|
rosterStartAt: Date | null;
|
|
nextRosterStartAt: Date | null;
|
|
};
|
|
newInformation: {
|
|
currentUserIdOnRoster: ObjectID | null;
|
|
rosterHandoffAt: Date | null;
|
|
nextUserIdOnRoster: ObjectID | null;
|
|
nextHandOffTimeAt: Date | null;
|
|
rosterStartAt: Date | null;
|
|
nextRosterStartAt: Date | null;
|
|
};
|
|
}): Promise<void> {
|
|
// Before we send any notification, we need to check if this schedule is attached to any on-call policy.
|
|
|
|
const escalationRulesAttachedToSchedule: Array<OnCallDutyPolicyEscalationRuleSchedule> =
|
|
await OnCallDutyPolicyEscalationRuleScheduleService.findBy({
|
|
query: {
|
|
onCallDutyPolicyScheduleId: data.scheduleId,
|
|
},
|
|
select: {
|
|
projectId: true,
|
|
_id: true,
|
|
onCallDutyPolicy: {
|
|
name: true,
|
|
_id: true,
|
|
},
|
|
onCallDutyPolicyEscalationRule: {
|
|
name: true,
|
|
_id: true,
|
|
order: true,
|
|
},
|
|
onCallDutyPolicySchedule: {
|
|
name: true,
|
|
_id: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
});
|
|
|
|
if (escalationRulesAttachedToSchedule.length === 0) {
|
|
// do nothing.
|
|
return;
|
|
}
|
|
|
|
for (const escalationRule of escalationRulesAttachedToSchedule) {
|
|
const projectId: ObjectID = escalationRule.projectId!;
|
|
|
|
const onCallSchedule: OnCallDutyPolicySchedule | undefined =
|
|
escalationRule.onCallDutyPolicySchedule;
|
|
|
|
if (!onCallSchedule) {
|
|
continue;
|
|
}
|
|
|
|
const onCallPolicy: OnCallDutyPolicy | undefined =
|
|
escalationRule.onCallDutyPolicy;
|
|
|
|
if (!onCallPolicy) {
|
|
continue;
|
|
}
|
|
|
|
const onCallDutyPolicyEscalationRule:
|
|
| OnCallDutyPolicyEscalationRule
|
|
| undefined = escalationRule.onCallDutyPolicyEscalationRule;
|
|
|
|
if (!onCallDutyPolicyEscalationRule) {
|
|
continue;
|
|
}
|
|
|
|
const { previousInformation, newInformation } = data;
|
|
|
|
/*
|
|
* if there's a change, witht he current user, send notification to the new current user.
|
|
* Send notificiation to the new current user.
|
|
*/
|
|
if (
|
|
previousInformation.currentUserIdOnRoster?.toString() !==
|
|
newInformation.currentUserIdOnRoster?.toString() ||
|
|
previousInformation.rosterHandoffAt?.toString() !==
|
|
newInformation.rosterHandoffAt?.toString()
|
|
) {
|
|
if (
|
|
previousInformation.currentUserIdOnRoster?.toString() !==
|
|
newInformation.currentUserIdOnRoster?.toString() &&
|
|
previousInformation.currentUserIdOnRoster?.toString()
|
|
) {
|
|
// the user has changed. Send notifiction to old user that he has been removed.
|
|
|
|
// send notification to the new current user.
|
|
|
|
const sendEmailToUserId: ObjectID =
|
|
previousInformation.currentUserIdOnRoster;
|
|
|
|
const userTimezone: Timezone | null =
|
|
await UserService.getTimezoneForUser(sendEmailToUserId);
|
|
|
|
const vars: Dictionary<string> = {
|
|
onCallPolicyName: onCallPolicy.name || "No name provided",
|
|
escalationRuleName:
|
|
onCallDutyPolicyEscalationRule.name || "No name provided",
|
|
escalationRuleOrder:
|
|
onCallDutyPolicyEscalationRule.order?.toString() || "-",
|
|
reason:
|
|
"Your on-call roster on schedule " +
|
|
onCallSchedule.name +
|
|
" just ended.",
|
|
rosterStartsAt:
|
|
OneUptimeDate.getDateAsFormattedHTMLInMultipleTimezones({
|
|
date: previousInformation.rosterStartAt!,
|
|
timezones: userTimezone ? [userTimezone] : [Timezone.GMT],
|
|
}),
|
|
rosterEndsAt:
|
|
OneUptimeDate.getDateAsFormattedHTMLInMultipleTimezones({
|
|
date: OneUptimeDate.isInTheFuture(
|
|
previousInformation.rosterHandoffAt!,
|
|
)
|
|
? OneUptimeDate.getCurrentDate()
|
|
: previousInformation.rosterHandoffAt!,
|
|
timezones: userTimezone ? [userTimezone] : [Timezone.GMT],
|
|
}),
|
|
onCallPolicyViewLink: (
|
|
await OnCallDutyPolicyService.getOnCallDutyPolicyLinkInDashboard(
|
|
projectId,
|
|
onCallPolicy.id!,
|
|
)
|
|
).toString(),
|
|
};
|
|
|
|
// current user changed, send alert the new current user.
|
|
const emailMessage: EmailEnvelope = {
|
|
templateType: EmailTemplateType.UserNoLongerActiveOnOnCallRoster,
|
|
vars: vars,
|
|
subject: "You are no longer on-call for " + onCallPolicy.name!,
|
|
};
|
|
|
|
const sms: SMSMessage = {
|
|
message: `This is a message from OneUptime. You are no longer on-call for ${onCallPolicy.name!} because your on-call roster on schedule ${onCallSchedule.name} just ended. To unsubscribe from this notification go to User Settings in OneUptime Dashboard.`,
|
|
};
|
|
|
|
const callMessage: CallRequestMessage = {
|
|
data: [
|
|
{
|
|
sayMessage: `This is a message from OneUptime. You are no longer on-call for ${onCallPolicy.name!} because your on-call roster on schedule ${onCallSchedule.name} just ended. To unsubscribe from this notification go to User Settings in OneUptime Dashboard. Good bye.`,
|
|
},
|
|
],
|
|
};
|
|
|
|
const pushMessage: PushNotificationMessage =
|
|
PushNotificationUtil.createGenericNotification({
|
|
title: "On-Call Duty Ended",
|
|
body: `You are no longer on-call for ${onCallPolicy.name!} as your roster on schedule ${onCallSchedule.name} has ended.`,
|
|
tag: "on-call-duty-ended",
|
|
requireInteraction: false,
|
|
});
|
|
|
|
const eventType: NotificationSettingEventType =
|
|
NotificationSettingEventType.SEND_WHEN_USER_IS_NO_LONGER_ACTIVE_ON_ON_CALL_ROSTER;
|
|
|
|
const whatsAppMessage: WhatsAppMessagePayload =
|
|
createWhatsAppMessageFromTemplate({
|
|
eventType,
|
|
templateVariables: {
|
|
on_call_policy_name: onCallPolicy.name!,
|
|
schedule_name: onCallSchedule.name!,
|
|
schedule_link: vars["onCallPolicyViewLink"] || "",
|
|
},
|
|
});
|
|
|
|
await UserNotificationSettingService.sendUserNotification({
|
|
userId: sendEmailToUserId,
|
|
projectId: projectId,
|
|
emailEnvelope: emailMessage,
|
|
smsMessage: sms,
|
|
callRequestMessage: callMessage,
|
|
pushNotificationMessage: pushMessage,
|
|
whatsAppMessage,
|
|
eventType,
|
|
onCallPolicyId: escalationRule.onCallDutyPolicy!.id!,
|
|
onCallPolicyEscalationRuleId:
|
|
escalationRule.onCallDutyPolicyEscalationRule!.id!,
|
|
onCallScheduleId: data.scheduleId,
|
|
});
|
|
|
|
// add end log for user.
|
|
OnCallDutyPolicyTimeLogService.endTimeLogForUser({
|
|
userId: sendEmailToUserId,
|
|
onCallDutyPolicyScheduleId: data.scheduleId,
|
|
onCallDutyPolicyEscalationRuleId:
|
|
escalationRule.onCallDutyPolicyEscalationRule!.id!,
|
|
onCallDutyPolicyId: escalationRule.onCallDutyPolicy!.id!,
|
|
projectId: projectId,
|
|
endsAt: OneUptimeDate.getCurrentDate(),
|
|
}).catch((err: Error) => {
|
|
logger.error(
|
|
"Error ending time log for user: " +
|
|
sendEmailToUserId.toString() +
|
|
" for schedule: " +
|
|
data.scheduleId.toString(),
|
|
);
|
|
logger.error(err);
|
|
});
|
|
|
|
const onCallDutyPolicyId: ObjectID =
|
|
escalationRule.onCallDutyPolicy!.id!;
|
|
|
|
// Send workspace notifiction as well.
|
|
await OnCallDutyPolicyFeedService.createOnCallDutyPolicyFeedItem({
|
|
onCallDutyPolicyId: onCallDutyPolicyId,
|
|
projectId: projectId!,
|
|
onCallDutyPolicyFeedEventType:
|
|
OnCallDutyPolicyFeedEventType.RosterHandoff,
|
|
displayColor: Green500,
|
|
feedInfoInMarkdown: `🚫 **${await UserService.getUserMarkdownString(
|
|
{
|
|
userId: sendEmailToUserId,
|
|
projectId: projectId!,
|
|
},
|
|
)}** is no longer on call for [On-Call Policy ${escalationRule.onCallDutyPolicy?.name}](${(await OnCallDutyPolicyService.getOnCallDutyPolicyLinkInDashboard(projectId!, onCallDutyPolicyId!)).toString()}) escalation rule **${escalationRule.onCallDutyPolicyEscalationRule?.name}** with order **${escalationRule.onCallDutyPolicyEscalationRule?.order}** because your on-call roster on schedule **${onCallSchedule.name}** just ended.`,
|
|
userId: sendEmailToUserId || undefined,
|
|
workspaceNotification: {
|
|
sendWorkspaceNotification: true,
|
|
notifyUserId: undefined,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (newInformation.currentUserIdOnRoster?.toString()) {
|
|
// send email to the new current user.
|
|
const sendEmailToUserId: ObjectID =
|
|
newInformation.currentUserIdOnRoster;
|
|
const userTimezone: Timezone | null =
|
|
await UserService.getTimezoneForUser(sendEmailToUserId);
|
|
|
|
const vars: Dictionary<string> = {
|
|
onCallPolicyName: onCallPolicy.name || "No name provided",
|
|
escalationRuleName:
|
|
onCallDutyPolicyEscalationRule.name || "No name provided",
|
|
escalationRuleOrder:
|
|
onCallDutyPolicyEscalationRule.order?.toString() || "-",
|
|
reason:
|
|
"You are now on-call for the policy " +
|
|
onCallPolicy.name +
|
|
" because your on-call roster on schedule " +
|
|
onCallSchedule.name,
|
|
rosterStartsAt:
|
|
OneUptimeDate.getDateAsFormattedHTMLInMultipleTimezones({
|
|
date: newInformation.rosterStartAt!,
|
|
timezones: userTimezone ? [userTimezone] : [Timezone.GMT],
|
|
}),
|
|
rosterEndsAt:
|
|
OneUptimeDate.getDateAsFormattedHTMLInMultipleTimezones({
|
|
date: newInformation.rosterHandoffAt!,
|
|
timezones: userTimezone ? [userTimezone] : [Timezone.GMT],
|
|
}),
|
|
onCallPolicyViewLink: (
|
|
await OnCallDutyPolicyService.getOnCallDutyPolicyLinkInDashboard(
|
|
projectId,
|
|
onCallPolicy.id!,
|
|
)
|
|
).toString(),
|
|
};
|
|
|
|
const emailMessage: EmailEnvelope = {
|
|
templateType: EmailTemplateType.UserCurrentlyOnOnCallRoster,
|
|
vars: vars,
|
|
subject: "You are now on-call for " + onCallPolicy.name!,
|
|
};
|
|
|
|
const sms: SMSMessage = {
|
|
message: `This is a message from OneUptime. You are now on-call for ${onCallPolicy.name!} because you are now on the roster for schedule ${onCallSchedule.name}. To unsubscribe from this notification go to User Settings in OneUptime Dashboard.`,
|
|
};
|
|
|
|
const callMessage: CallRequestMessage = {
|
|
data: [
|
|
{
|
|
sayMessage: `This is a message from OneUptime. You are now on-call for ${onCallPolicy.name!} because you are now on the roster for schedule ${onCallSchedule.name}. To unsubscribe from this notification go to User Settings in OneUptime Dashboard. Good bye.`,
|
|
},
|
|
],
|
|
};
|
|
|
|
const pushMessage: PushNotificationMessage =
|
|
PushNotificationUtil.createGenericNotification({
|
|
title: "On-Call Duty Started",
|
|
body: `You are now on-call for ${onCallPolicy.name!} on schedule ${onCallSchedule.name}.`,
|
|
tag: "on-call-duty-started",
|
|
requireInteraction: true,
|
|
});
|
|
|
|
const eventType: NotificationSettingEventType =
|
|
NotificationSettingEventType.SEND_WHEN_USER_IS_ON_CALL_ROSTER;
|
|
|
|
const whatsAppMessage: WhatsAppMessagePayload =
|
|
createWhatsAppMessageFromTemplate({
|
|
eventType,
|
|
templateVariables: {
|
|
on_call_policy_name: onCallPolicy.name!,
|
|
schedule_name: onCallSchedule.name!,
|
|
schedule_link: vars["onCallPolicyViewLink"] || "",
|
|
},
|
|
});
|
|
|
|
await UserNotificationSettingService.sendUserNotification({
|
|
userId: sendEmailToUserId,
|
|
projectId: projectId,
|
|
emailEnvelope: emailMessage,
|
|
smsMessage: sms,
|
|
callRequestMessage: callMessage,
|
|
pushNotificationMessage: pushMessage,
|
|
whatsAppMessage,
|
|
eventType,
|
|
onCallPolicyId: escalationRule.onCallDutyPolicy!.id!,
|
|
onCallPolicyEscalationRuleId:
|
|
escalationRule.onCallDutyPolicyEscalationRule!.id!,
|
|
onCallScheduleId: data.scheduleId,
|
|
});
|
|
|
|
// add start log for user.
|
|
OnCallDutyPolicyTimeLogService.startTimeLogForUser({
|
|
userId: sendEmailToUserId,
|
|
onCallDutyPolicyScheduleId: data.scheduleId,
|
|
onCallDutyPolicyEscalationRuleId:
|
|
escalationRule.onCallDutyPolicyEscalationRule!.id!,
|
|
onCallDutyPolicyId: escalationRule.onCallDutyPolicy!.id!,
|
|
projectId: projectId,
|
|
startsAt: OneUptimeDate.getCurrentDate(),
|
|
}).catch((err: Error) => {
|
|
logger.error(
|
|
"Error starting time log for user: " +
|
|
sendEmailToUserId.toString() +
|
|
" for schedule: " +
|
|
data.scheduleId.toString(),
|
|
);
|
|
logger.error(err);
|
|
});
|
|
|
|
const onCallDutyPolicyId: ObjectID =
|
|
escalationRule.onCallDutyPolicy!.id!;
|
|
|
|
// Send workspace notifiction as well.
|
|
await OnCallDutyPolicyFeedService.createOnCallDutyPolicyFeedItem({
|
|
onCallDutyPolicyId: onCallDutyPolicyId,
|
|
projectId: projectId!,
|
|
onCallDutyPolicyFeedEventType:
|
|
OnCallDutyPolicyFeedEventType.RosterHandoff,
|
|
displayColor: Green500,
|
|
feedInfoInMarkdown: `📞 **${await UserService.getUserMarkdownString(
|
|
{
|
|
userId: sendEmailToUserId,
|
|
projectId: projectId!,
|
|
},
|
|
)}** is currently on call for [On-Call Policy ${escalationRule.onCallDutyPolicy?.name}](${(await OnCallDutyPolicyService.getOnCallDutyPolicyLinkInDashboard(projectId!, onCallDutyPolicyId!)).toString()}) escalation rule **${escalationRule.onCallDutyPolicyEscalationRule?.name}** with order **${escalationRule.onCallDutyPolicyEscalationRule?.order}** because of schedule **${onCallSchedule.name}** and your on-call roster starts at **${OneUptimeDate.getDateAsFormattedStringInMultipleTimezones(
|
|
{
|
|
date: newInformation.rosterStartAt!,
|
|
timezones: userTimezone ? [userTimezone] : [Timezone.GMT],
|
|
},
|
|
)}** and ends at **${OneUptimeDate.getDateAsFormattedStringInMultipleTimezones(
|
|
{
|
|
date: newInformation.rosterHandoffAt!,
|
|
timezones: userTimezone ? [userTimezone] : [Timezone.GMT],
|
|
},
|
|
)}**.`,
|
|
userId: sendEmailToUserId || undefined,
|
|
workspaceNotification: {
|
|
sendWorkspaceNotification: true,
|
|
notifyUserId: undefined,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// send an email to the next user.
|
|
if (
|
|
previousInformation.nextUserIdOnRoster?.toString() !==
|
|
newInformation.nextUserIdOnRoster?.toString() ||
|
|
previousInformation.nextHandOffTimeAt?.toString() !==
|
|
newInformation.nextHandOffTimeAt?.toString() ||
|
|
previousInformation.nextRosterStartAt?.toString() !==
|
|
newInformation.nextRosterStartAt?.toString()
|
|
) {
|
|
if (newInformation.nextUserIdOnRoster?.toString()) {
|
|
// send email to the next user.
|
|
const sendEmailToUserId: ObjectID = newInformation.nextUserIdOnRoster;
|
|
const userTimezone: Timezone | null =
|
|
await UserService.getTimezoneForUser(sendEmailToUserId);
|
|
|
|
const vars: Dictionary<string> = {
|
|
onCallPolicyName: onCallPolicy.name || "No name provided",
|
|
escalationRuleName:
|
|
onCallDutyPolicyEscalationRule.name || "No name provided",
|
|
escalationRuleOrder:
|
|
onCallDutyPolicyEscalationRule.order?.toString() || "-",
|
|
reason:
|
|
"You are next on-call for the policy " +
|
|
onCallPolicy.name +
|
|
" because your on-call roster on schedule " +
|
|
onCallSchedule.name +
|
|
" will start when the next handoff happens.",
|
|
rosterStartsAt:
|
|
OneUptimeDate.getDateAsFormattedHTMLInMultipleTimezones({
|
|
date: newInformation.nextRosterStartAt!,
|
|
timezones: userTimezone ? [userTimezone] : [Timezone.GMT],
|
|
}),
|
|
rosterEndsAt:
|
|
OneUptimeDate.getDateAsFormattedHTMLInMultipleTimezones({
|
|
date: newInformation.nextHandOffTimeAt!,
|
|
timezones: userTimezone ? [userTimezone] : [Timezone.GMT],
|
|
}),
|
|
onCallPolicyViewLink: (
|
|
await OnCallDutyPolicyService.getOnCallDutyPolicyLinkInDashboard(
|
|
projectId,
|
|
onCallPolicy.id!,
|
|
)
|
|
).toString(),
|
|
};
|
|
|
|
const emailMessage: EmailEnvelope = {
|
|
templateType: EmailTemplateType.UserNextOnOnCallRoster,
|
|
vars: vars,
|
|
subject: "You are next on-call for " + onCallPolicy.name!,
|
|
};
|
|
|
|
const sms: SMSMessage = {
|
|
message: `This is a message from OneUptime. You are next on-call for ${onCallPolicy.name!} because your on-call roster on schedule ${onCallSchedule.name} will start when the next handoff happens. To unsubscribe from this notification go to User Settings in OneUptime Dashboard.`,
|
|
};
|
|
|
|
const callMessage: CallRequestMessage = {
|
|
data: [
|
|
{
|
|
sayMessage: `This is a message from OneUptime. You are next on-call for ${onCallPolicy.name!} because your on-call roster on schedule ${onCallSchedule.name} will start when the next handoff happens. To unsubscribe from this notification go to User Settings in OneUptime Dashboard. Good bye.`,
|
|
},
|
|
],
|
|
};
|
|
|
|
const pushMessage: PushNotificationMessage =
|
|
PushNotificationUtil.createGenericNotification({
|
|
title: "Next On-Call Duty",
|
|
body: `You are next on-call for ${onCallPolicy.name!} on schedule ${onCallSchedule.name}.`,
|
|
tag: "next-on-call-duty",
|
|
requireInteraction: false,
|
|
});
|
|
|
|
const eventType: NotificationSettingEventType =
|
|
NotificationSettingEventType.SEND_WHEN_USER_IS_NEXT_ON_CALL_ROSTER;
|
|
|
|
const whatsAppMessage: WhatsAppMessagePayload =
|
|
createWhatsAppMessageFromTemplate({
|
|
eventType,
|
|
templateVariables: {
|
|
on_call_policy_name: onCallPolicy.name!,
|
|
schedule_name: onCallSchedule.name!,
|
|
schedule_link: vars["onCallPolicyViewLink"] || "",
|
|
},
|
|
});
|
|
|
|
await UserNotificationSettingService.sendUserNotification({
|
|
userId: sendEmailToUserId,
|
|
projectId: projectId,
|
|
emailEnvelope: emailMessage,
|
|
smsMessage: sms,
|
|
callRequestMessage: callMessage,
|
|
pushNotificationMessage: pushMessage,
|
|
whatsAppMessage,
|
|
eventType,
|
|
onCallPolicyId: escalationRule.onCallDutyPolicy!.id!,
|
|
onCallPolicyEscalationRuleId:
|
|
escalationRule.onCallDutyPolicyEscalationRule!.id!,
|
|
onCallScheduleId: data.scheduleId,
|
|
});
|
|
|
|
const onCallDutyPolicyId: ObjectID =
|
|
escalationRule.onCallDutyPolicy!.id!;
|
|
|
|
// Send workspace notifiction as well.
|
|
await OnCallDutyPolicyFeedService.createOnCallDutyPolicyFeedItem({
|
|
onCallDutyPolicyId: onCallDutyPolicyId,
|
|
projectId: projectId!,
|
|
onCallDutyPolicyFeedEventType:
|
|
OnCallDutyPolicyFeedEventType.RosterHandoff,
|
|
displayColor: Green500,
|
|
feedInfoInMarkdown: `➡️ **${await UserService.getUserMarkdownString(
|
|
{
|
|
userId: sendEmailToUserId,
|
|
projectId: projectId!,
|
|
},
|
|
)}** is next on call for [On-Call Policy ${escalationRule.onCallDutyPolicy?.name}](${(await OnCallDutyPolicyService.getOnCallDutyPolicyLinkInDashboard(projectId!, onCallDutyPolicyId!)).toString()}) escalation rule **${escalationRule.onCallDutyPolicyEscalationRule?.name}** with order **${escalationRule.onCallDutyPolicyEscalationRule?.order}**. The on-call roster on schedule **${onCallSchedule.name}** will start when the next handoff happens which is at **${OneUptimeDate.getDateAsFormattedStringInMultipleTimezones(
|
|
{
|
|
date: newInformation.nextRosterStartAt!,
|
|
timezones: userTimezone ? [userTimezone] : [Timezone.GMT],
|
|
},
|
|
)}** and will end at **${OneUptimeDate.getDateAsFormattedStringInMultipleTimezones(
|
|
{
|
|
date: newInformation.nextHandOffTimeAt!,
|
|
timezones: userTimezone ? [userTimezone] : [Timezone.GMT],
|
|
},
|
|
)}**.`,
|
|
userId: sendEmailToUserId || undefined,
|
|
workspaceNotification: {
|
|
sendWorkspaceNotification: true,
|
|
notifyUserId: undefined,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async refreshCurrentUserIdAndHandoffTimeInSchedule(
|
|
scheduleId: ObjectID,
|
|
): Promise<{
|
|
currentUserId: ObjectID | null;
|
|
handOffTimeAt: Date | null;
|
|
nextUserId: ObjectID | null;
|
|
nextHandOffTimeAt: Date | null;
|
|
rosterStartAt: Date | null;
|
|
nextRosterStartAt: Date | null;
|
|
}> {
|
|
logger.debug(
|
|
"refreshCurrentUserIdAndHandoffTimeInSchedule called with scheduleId: " +
|
|
scheduleId.toString(),
|
|
);
|
|
|
|
// get previoius result.
|
|
logger.debug(
|
|
"Fetching previous schedule information for scheduleId: " +
|
|
scheduleId.toString(),
|
|
);
|
|
const onCallSchedule: OnCallDutyPolicySchedule | null =
|
|
await this.findOneById({
|
|
id: scheduleId,
|
|
select: {
|
|
currentUserIdOnRoster: true,
|
|
rosterHandoffAt: true,
|
|
nextUserIdOnRoster: true,
|
|
rosterNextHandoffAt: true,
|
|
rosterStartAt: true,
|
|
rosterNextStartAt: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!onCallSchedule) {
|
|
logger.debug(
|
|
"Schedule not found for scheduleId: " + scheduleId.toString(),
|
|
);
|
|
throw new BadDataException("Schedule not found");
|
|
}
|
|
|
|
logger.debug(
|
|
"Previous schedule information fetched for scheduleId: " +
|
|
scheduleId.toString(),
|
|
);
|
|
|
|
const previousInformation: {
|
|
currentUserIdOnRoster: ObjectID | null;
|
|
rosterHandoffAt: Date | null;
|
|
nextUserIdOnRoster: ObjectID | null;
|
|
nextHandOffTimeAt: Date | null;
|
|
rosterStartAt: Date | null;
|
|
nextRosterStartAt: Date | null;
|
|
} = {
|
|
currentUserIdOnRoster: onCallSchedule.currentUserIdOnRoster || null,
|
|
rosterHandoffAt: onCallSchedule.rosterHandoffAt || null,
|
|
nextUserIdOnRoster: onCallSchedule.nextUserIdOnRoster || null,
|
|
nextHandOffTimeAt: onCallSchedule.rosterNextHandoffAt || null,
|
|
rosterStartAt: onCallSchedule.rosterStartAt || null,
|
|
nextRosterStartAt: onCallSchedule.rosterNextStartAt || null,
|
|
};
|
|
|
|
logger.debug(previousInformation);
|
|
|
|
logger.debug(
|
|
"Fetching new schedule information for scheduleId: " +
|
|
scheduleId.toString(),
|
|
);
|
|
|
|
const newInformation: {
|
|
currentUserId: ObjectID | null;
|
|
handOffTimeAt: Date | null;
|
|
nextUserId: ObjectID | null;
|
|
nextHandOffTimeAt: Date | null;
|
|
rosterStartAt: Date | null;
|
|
nextRosterStartAt: Date | null;
|
|
} = await this.getCurrrentUserIdAndHandoffTimeInSchedule(scheduleId);
|
|
|
|
logger.debug(newInformation);
|
|
|
|
logger.debug(
|
|
"Updating schedule with new information for scheduleId: " +
|
|
scheduleId.toString(),
|
|
);
|
|
|
|
await this.updateOneById({
|
|
id: scheduleId!,
|
|
data: {
|
|
currentUserIdOnRoster: newInformation.currentUserId,
|
|
rosterHandoffAt: newInformation.handOffTimeAt,
|
|
nextUserIdOnRoster: newInformation.nextUserId,
|
|
rosterNextHandoffAt: newInformation.nextHandOffTimeAt,
|
|
rosterStartAt: newInformation.rosterStartAt,
|
|
rosterNextStartAt: newInformation.nextRosterStartAt,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
|
|
logger.debug(
|
|
"Sending notifications for schedule handoff for scheduleId: " +
|
|
scheduleId.toString(),
|
|
);
|
|
|
|
// send notification to the users.
|
|
await this.sendNotificationToUserOnScheduleHandoff({
|
|
scheduleId: scheduleId,
|
|
previousInformation: previousInformation,
|
|
newInformation: {
|
|
currentUserIdOnRoster: newInformation.currentUserId,
|
|
rosterHandoffAt: newInformation.handOffTimeAt,
|
|
nextUserIdOnRoster: newInformation.nextUserId,
|
|
nextHandOffTimeAt: newInformation.nextHandOffTimeAt,
|
|
rosterStartAt: newInformation.rosterStartAt,
|
|
nextRosterStartAt: newInformation.nextRosterStartAt,
|
|
},
|
|
});
|
|
|
|
logger.debug(
|
|
"Returning new schedule information for scheduleId: " +
|
|
scheduleId.toString(),
|
|
);
|
|
|
|
return newInformation;
|
|
}
|
|
|
|
public async getCurrrentUserIdAndHandoffTimeInSchedule(
|
|
scheduleId: ObjectID,
|
|
): Promise<{
|
|
rosterStartAt: Date | null;
|
|
currentUserId: ObjectID | null;
|
|
handOffTimeAt: Date | null;
|
|
nextUserId: ObjectID | null;
|
|
nextHandOffTimeAt: Date | null;
|
|
nextRosterStartAt: Date | null;
|
|
}> {
|
|
logger.debug(
|
|
"getCurrrentUserIdAndHandoffTimeInSchedule called with scheduleId: " +
|
|
scheduleId.toString(),
|
|
);
|
|
|
|
const resultReturn: {
|
|
rosterStartAt: Date | null;
|
|
currentUserId: ObjectID | null;
|
|
handOffTimeAt: Date | null;
|
|
nextUserId: ObjectID | null;
|
|
nextHandOffTimeAt: Date | null;
|
|
nextRosterStartAt: Date | null;
|
|
} = {
|
|
currentUserId: null,
|
|
handOffTimeAt: null,
|
|
nextUserId: null,
|
|
nextHandOffTimeAt: null,
|
|
rosterStartAt: null,
|
|
nextRosterStartAt: null,
|
|
};
|
|
|
|
logger.debug("Fetching events for scheduleId: " + scheduleId.toString());
|
|
const events: Array<CalendarEvent> | null =
|
|
await this.getEventByIndexInSchedule({
|
|
scheduleId: scheduleId,
|
|
getNumberOfEvents: 2,
|
|
});
|
|
|
|
logger.debug("Events fetched: " + JSON.stringify(events));
|
|
|
|
let currentEvent: CalendarEvent | null = events[0] || null;
|
|
let nextEvent: CalendarEvent | null = events[1] || null;
|
|
|
|
logger.debug("Current event: " + JSON.stringify(currentEvent));
|
|
logger.debug("Next event: " + JSON.stringify(nextEvent));
|
|
|
|
// if the current event start time in the future then the current event is the next event.
|
|
if (currentEvent && OneUptimeDate.isInTheFuture(currentEvent.start)) {
|
|
logger.debug(
|
|
"Current event is in the future, treating it as next event.",
|
|
);
|
|
nextEvent = currentEvent;
|
|
currentEvent = null;
|
|
}
|
|
|
|
if (currentEvent) {
|
|
logger.debug("Processing current event: " + JSON.stringify(currentEvent));
|
|
const userId: string | undefined = currentEvent?.title; // this is user id in string.
|
|
|
|
if (userId) {
|
|
logger.debug("Current userId: " + userId);
|
|
resultReturn.currentUserId = new ObjectID(userId);
|
|
}
|
|
|
|
// get handOffTime
|
|
const handOffTime: Date | undefined = currentEvent?.end; // this is user id in string.
|
|
if (handOffTime) {
|
|
logger.debug("Current handOffTime: " + handOffTime.toISOString());
|
|
resultReturn.handOffTimeAt = handOffTime;
|
|
}
|
|
|
|
// get start time
|
|
const startTime: Date | undefined = currentEvent?.start; // this is user id in string.
|
|
if (startTime) {
|
|
logger.debug("Current rosterStartAt: " + startTime.toISOString());
|
|
resultReturn.rosterStartAt = startTime;
|
|
}
|
|
}
|
|
|
|
// do the same for next event.
|
|
|
|
if (nextEvent) {
|
|
logger.debug("Processing next event: " + JSON.stringify(nextEvent));
|
|
const userId: string | undefined = nextEvent?.title; // this is user id in string.
|
|
|
|
if (userId) {
|
|
logger.debug("Next userId: " + userId);
|
|
resultReturn.nextUserId = new ObjectID(userId);
|
|
}
|
|
|
|
// get handOffTime
|
|
const handOffTime: Date | undefined = nextEvent?.end; // this is user id in string.
|
|
if (handOffTime) {
|
|
logger.debug("Next handOffTime: " + handOffTime.toISOString());
|
|
resultReturn.nextHandOffTimeAt = handOffTime;
|
|
}
|
|
|
|
// get start time
|
|
const startTime: Date | undefined = nextEvent?.start; // this is user id in string.
|
|
if (startTime) {
|
|
logger.debug("Next rosterStartAt: " + startTime.toISOString());
|
|
resultReturn.nextRosterStartAt = startTime;
|
|
}
|
|
}
|
|
|
|
logger.debug("Returning result: " + JSON.stringify(resultReturn));
|
|
return resultReturn;
|
|
}
|
|
|
|
private async getScheduleLayerProps(data: {
|
|
scheduleId: ObjectID;
|
|
}): Promise<Array<LayerProps>> {
|
|
// get schedule layers.
|
|
|
|
const scheduleId: ObjectID = data.scheduleId;
|
|
|
|
const layers: Array<OnCallDutyPolicyScheduleLayer> =
|
|
await OnCallDutyPolicyScheduleLayerService.findBy({
|
|
query: {
|
|
onCallDutyPolicyScheduleId: scheduleId,
|
|
},
|
|
select: {
|
|
order: true,
|
|
name: true,
|
|
description: true,
|
|
startsAt: true,
|
|
restrictionTimes: true,
|
|
rotation: true,
|
|
onCallDutyPolicyScheduleId: true,
|
|
projectId: true,
|
|
handOffTime: true,
|
|
},
|
|
sort: {
|
|
order: SortOrder.Ascending,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
});
|
|
|
|
const layerUsers: Array<OnCallDutyPolicyScheduleLayerUser> =
|
|
await OnCallDutyPolicyScheduleLayerUserService.findBy({
|
|
query: {
|
|
onCallDutyPolicyScheduleId: scheduleId,
|
|
},
|
|
select: {
|
|
user: true,
|
|
order: true,
|
|
onCallDutyPolicyScheduleLayerId: true,
|
|
},
|
|
sort: {
|
|
order: SortOrder.Ascending,
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const layerProps: Array<LayerProps> = [];
|
|
|
|
for (const layer of layers) {
|
|
layerProps.push({
|
|
users:
|
|
layerUsers
|
|
.filter((layerUser: OnCallDutyPolicyScheduleLayerUser) => {
|
|
return (
|
|
layerUser.onCallDutyPolicyScheduleLayerId?.toString() ===
|
|
layer.id?.toString()
|
|
);
|
|
})
|
|
.map((layerUser: OnCallDutyPolicyScheduleLayerUser) => {
|
|
return layerUser.user!;
|
|
})
|
|
.filter((user: User) => {
|
|
return Boolean(user);
|
|
}) || [],
|
|
startDateTimeOfLayer: layer.startsAt!,
|
|
restrictionTimes: layer.restrictionTimes!,
|
|
rotation: layer.rotation!,
|
|
handOffTime: layer.handOffTime!,
|
|
});
|
|
}
|
|
|
|
return layerProps;
|
|
}
|
|
|
|
public async getEventByIndexInSchedule(data: {
|
|
scheduleId: ObjectID;
|
|
getNumberOfEvents: number; // which event would you like to get. First event, second event, etc.
|
|
}): Promise<Array<CalendarEvent>> {
|
|
logger.debug(
|
|
"getEventByIndexInSchedule called with data: " + JSON.stringify(data),
|
|
);
|
|
|
|
const layerProps: Array<LayerProps> = await this.getScheduleLayerProps({
|
|
scheduleId: data.scheduleId,
|
|
});
|
|
|
|
logger.debug("Layer properties fetched: " + JSON.stringify(layerProps));
|
|
|
|
if (layerProps.length === 0) {
|
|
logger.debug(
|
|
"No layers found for scheduleId: " + data.scheduleId.toString(),
|
|
);
|
|
return [];
|
|
}
|
|
|
|
const currentStartTime: Date = OneUptimeDate.getCurrentDate();
|
|
logger.debug("Current start time: " + currentStartTime.toISOString());
|
|
|
|
const currentEndTime: Date = OneUptimeDate.addRemoveYears(
|
|
currentStartTime,
|
|
1,
|
|
);
|
|
logger.debug("Current end time: " + currentEndTime.toISOString());
|
|
|
|
const numberOfEventsToGet: number = data.getNumberOfEvents;
|
|
logger.debug("Number of events to get: " + numberOfEventsToGet);
|
|
|
|
const events: Array<CalendarEvent> = this.layerUtil.getMultiLayerEvents(
|
|
{
|
|
layers: layerProps,
|
|
calendarStartDate: currentStartTime,
|
|
calendarEndDate: currentEndTime,
|
|
},
|
|
{
|
|
getNumberOfEvents: numberOfEventsToGet,
|
|
},
|
|
);
|
|
|
|
logger.debug("Events fetched: " + JSON.stringify(events));
|
|
|
|
return events;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getCurrentUserIdInSchedule(
|
|
scheduleId: ObjectID,
|
|
): Promise<ObjectID | null> {
|
|
const layerProps: Array<LayerProps> = await this.getScheduleLayerProps({
|
|
scheduleId: scheduleId,
|
|
});
|
|
|
|
if (layerProps.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const currentStartTime: Date = OneUptimeDate.getCurrentDate();
|
|
const currentEndTime: Date = OneUptimeDate.addRemoveSeconds(
|
|
currentStartTime,
|
|
1,
|
|
);
|
|
|
|
const events: Array<CalendarEvent> = this.layerUtil.getMultiLayerEvents(
|
|
{
|
|
layers: layerProps,
|
|
calendarStartDate: currentStartTime,
|
|
calendarEndDate: currentEndTime,
|
|
},
|
|
{
|
|
getNumberOfEvents: 1,
|
|
},
|
|
);
|
|
|
|
const currentEvent: CalendarEvent | null = events[0] || null;
|
|
|
|
if (!currentEvent) {
|
|
return null;
|
|
}
|
|
|
|
const userId: string | undefined = currentEvent?.title; // this is user id in string.
|
|
|
|
if (!userId) {
|
|
return null;
|
|
}
|
|
|
|
return new ObjectID(userId);
|
|
}
|
|
}
|
|
|
|
export default new Service();
|