synt_backend
Version:
Synt light-weight node backend service
493 lines (410 loc) • 11.2 kB
JavaScript
import cron from "node-cron";
import * as db from "../mysql/models/index";
import { backendDebug } from "../helpers/debug";
import { notifyUsers, notifier } from "../helpers/notifier";
import { Op } from "sequelize";
import { parse } from "../helpers/parser";
const debug = backendDebug.extend("JobReminderManager");
export class JobReminderManager {
constructor() {
this._jobs = new Map();
}
/*
example of reminder {
id: 4,
message: 'test repeat every minute',
recipient: 'synt',
dateOfReceiving: {
type: 'simple',
repeatType: 'repeat',
count: 2,
unit: 'minute',
startingDate: '2022-10-10T17:00:00.000Z'
},
createdAt: 2022-10-10T17:13:59.000Z,
updatedAt: 2022-10-10T17:13:59.000Z,
users: 'admin@test.com,user@test.com',
VMEId: 3
}
*/
scheduleReminder(reminder) {
const parsedReminder =
typeof reminder.dateOfReceiving === "string"
? { ...reminder, dateOfReceiving: parse(reminder.dateOfReceiving) }
: reminder;
this.stopReminder(parsedReminder.id);
if (parsedReminder.active) {
this._startReminderJob(parsedReminder);
}
}
scheduleReminders(reminders) {
reminders.forEach((reminder) => {
this.scheduleReminder(reminder);
});
}
stopReminder(id) {
if (this._jobs.has(id)) {
this._jobs.get(id).stop();
this._jobs.delete(id);
}
}
_startReminderJob(reminder) {
if (reminder.dateOfReceiving.type === "simple") {
this._startReminderSimpleJob(reminder);
}
}
_startReminderSimpleJob(reminder) {
if (reminder.dateOfReceiving.repeatType === "before") {
this._startReminderSimpleBeforeJob(reminder);
} else if (reminder.dateOfReceiving.repeatType === "repeat") {
this._startReminderSimpleRepeatJob(reminder);
} else if (reminder.dateOfReceiving.repeatType === "one_time") {
this._startReminderSimpleOneTimeJob(reminder);
}
}
async _getReminderBeforeDates(reminder) {
if (reminder.dateOfReceiving.event === "financial_year") {
const fys = await db.FinancialYear.findAll({
where: {
VMEId: reminder.VMEId,
is_settled: 0,
},
});
return fys.map((fy) => new Date(fy.end_date));
}
if (reminder.dateOfReceiving.event === "general_meeting") {
const ms = await db.Meeting.findAll({
where: {
VMEId: reminder.VMEId,
type: "general",
},
});
return ms.map((m) => new Date(m.starts_at));
}
return [];
}
relativeDatesAreChanged({ vmeId, type }) {
if (type === "financial_year") {
this._relativeFinancialYearDatesAreChanged({ vmeId });
} else if (type === "general_meeting") {
this._relativeGeneralMeetingDatesAreChanged({ vmeId });
}
}
_findJobs(criterias) {
const jobs = [];
this._jobs.forEach((job) => {
if (criterias.every((criteria) => criteria(job))) {
jobs.push(job);
}
});
return jobs;
}
_relativeFinancialYearDatesAreChanged({ vmeId }) {
const jobs = this._findJobs([
jobCriteria.optionally(vmeId && jobCriteria.byVMEId(vmeId)),
jobCriteria.type("simple"),
jobCriteria.repeatType("before"),
jobCriteria.event("financial_year"),
]);
jobs.forEach((j) => {
this.scheduleReminder(j.reminder);
});
}
_relativeGeneralMeetingDatesAreChanged({ vmeId }) {
const jobs = this._findJobs([
jobCriteria.optionally(vmeId && jobCriteria.byVMEId(vmeId)),
jobCriteria.type("simple"),
jobCriteria.repeatType("before"),
jobCriteria.event("general_meeting"),
]);
jobs.forEach((j) => {
this.scheduleReminder(j.reminder);
});
}
async _handleReminderJob(reminder) {
// check the type of reminder for sending our the message
if (reminder.type === "message") {
// custom message to be sent
const data = await getUsersAndVMEFromReminder(reminder);
notifyUsers(data, "reminder", { Reminder: reminder });
} else if (reminder.type === "unpaid provision") {
// all unpaid provision need to be found to be sent
const VME = await db.VME.findOne({
where: {
id: reminder.VMEId,
paid_at: null,
},
});
db.Provision.findAll({
where: {
VMEId: VME.id,
},
include: [
{
model: db.User,
},
{
model: db.Lot,
},
{
model: db.ProvisionFile,
},
],
}).then((provisions) => {
provisions.forEach((P) => {
P.Users.forEach((U) => {
notifier.notify(U, "remind_provision", {
Provision: P,
VME,
});
});
});
});
}
}
_scheduleOneTimeJob(reminder, date) {
debug("Schedule one-time job at " + date.toISOString());
const job = runOnceAt(date, () => {
this._handleReminderJob(reminder);
});
this._cacheJob(reminder, () => {
job.stop();
});
}
async _startReminderSimpleBeforeJob(reminder) {
const dates = await this._getReminderBeforeDates(reminder);
dates.forEach((date) => {
const now = Date.now();
const eventTime = date.getTime();
const scheduledTime = eventTime - unitToMs(reminder.dateOfReceiving.unit);
if (scheduledTime >= now) {
this._scheduleOneTimeJob(reminder, new Date(scheduledTime));
}
});
}
_startReminderSimpleRepeatJob(reminder) {
const cronExpression = toRepeatCronExpression(reminder.dateOfReceiving);
if (cronExpression === "") {
return;
}
const now = Date.now();
const startingDate = new Date(reminder.dateOfReceiving.startingDate);
const startingDateTime = startingDate.getTime();
const scheduleJob = () => {
debug("Schedule repeatable job with cron " + cronExpression);
const job = cron.schedule(cronExpression, () => {
this._handleReminderJob(reminder);
});
this._cacheJob(reminder, () => {
job.stop();
});
};
if (startingDateTime - now > 0) {
debug(
"Schedule repeatable job with cron " +
cronExpression +
" starting at " +
startingDate.toISOString()
);
const job = runOnceAt(startingDate, () => scheduleJob());
this._cacheJob(reminder, () => {
job.stop();
});
} else {
scheduleJob();
}
}
_cacheJob(reminder, stop) {
this._jobs.set(reminder.id, {
reminder,
stop,
});
}
_startReminderSimpleOneTimeJob(reminder) {
const jobTime = new Date(reminder.dateOfReceiving.date);
const now = new Date();
if (jobTime >= now) {
this._scheduleOneTimeJob(
reminder,
new Date(reminder.dateOfReceiving.date)
);
}
}
}
// ----------------------------------------------------------------------------
const jobCriteria = {
byVMEId: (vmeId) => (job) => String(job.reminder.VMEId) === String(vmeId),
type: (type) => (job) => job.reminder.dateOfReceiving.type === type,
repeatType: (repeatType) => (job) =>
job.reminder.dateOfReceiving.repeatType === repeatType,
event: (event) => (job) => job.reminder.dateOfReceiving.event === event,
optionally: (criteria) => (job) =>
typeof criteria === "function" ? criteria(job) : true,
};
// ----------------------------------------------------------------------------
// ["minute", "hour", "day", "week", "month", "year"];
function toRepeatCronExpression({ unit, count }) {
if (unit === "minute") {
return `*/${count} * * * *`;
}
if (unit === "hour") {
return `0 */${count} * * *`;
}
if (unit === "day") {
return `0 0 */${count} * *`;
}
if (unit === "week") {
// TODO
}
if (unit === "month") {
return `0 0 1 */${count} *`;
}
if (unit === "year") {
// TODO
}
return "";
}
// ----------------------------------------------------------------------------
const minute = 1000 * 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
const month = day * 30;
const year = month * 12;
const unit_to_ms = {
minute,
hour,
day,
week,
month,
year,
};
function unitToMs(unit) {
const result = unit_to_ms[unit] ?? 0;
return result;
}
function dateToCron(date) {
const minutes = date.getMinutes();
const hours = date.getHours();
const days = date.getDate();
const months = date.getMonth() + 1;
return `${minutes} ${hours} ${days} ${months} *`;
}
export const jobReminderManager = new JobReminderManager();
// ----------------------------------------------------------------------------
function runOnceAt(datetime, jobHandler) {
if (datetime > new Date()) {
const job = cron.schedule(dateToCron(datetime), () => {
jobHandler();
job.stop();
});
return {
stop: () => {
job.stop();
},
};
}
return {
stop: () => {},
};
}
// ----------------------------------------------------------------------------
async function getUsersAndVMEFromReminder(reminder) {
const VME = await db.VME.findOne({
where: {
id: reminder.VMEId,
},
});
if (!VME) {
return null;
}
const Users = await getUsersFromReminder(VME, reminder);
return {
VME,
Users,
};
}
async function getCustomUsers(VME, reminder) {
const emails = reminder.users.split(",");
const UserVMEs = await db.UserVME.findAll({
where: {
VMEId: VME.id,
is_disabled: false,
},
include: [
{
model: db.User,
where: {
email: {
[Op.in]: emails,
},
},
as: "User",
},
],
});
return UserVMEs.map((uv) => uv.User);
}
async function getSyntUsers(VME) {
const UserVMEs = await db.UserVME.findAll({
where: {
VMEId: VME.id,
type: "synt_authoriser",
is_disabled: false,
},
include: [
{
model: db.User,
as: "User",
},
],
});
return UserVMEs.map((uv) => uv.User);
}
async function getOwnerUsers(VME) {
const UserVMEs = await db.UserVME.findAll({
where: {
VMEId: VME.id,
type: "commissioner",
is_disabled: false,
},
include: [
{
model: db.User,
as: "User",
},
],
});
return UserVMEs.map((uv) => uv.User);
}
async function getEveryoneUsers(VME) {
const UserVMEs = await db.UserVME.findAll({
where: {
VMEId: VME.id,
is_disabled: false,
},
include: [
{
model: db.User,
as: "User",
},
],
});
return UserVMEs.map((uv) => uv.User);
}
async function getUsersFromReminder(VME, reminder) {
if (reminder.recipient === "custom") {
return await getCustomUsers(VME, reminder);
}
if (reminder.recipient === "synt") {
return await getSyntUsers(VME);
}
if (reminder.recipient === "owners") {
return await getOwnerUsers(VME);
}
if (reminder.recipient === "everyone") {
return await getEveryoneUsers(VME);
}
return [];
}
// ----------------------------------------------------------------------------