UNPKG

synt_backend

Version:

Synt light-weight node backend service

493 lines (410 loc) 11.2 kB
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 []; } // ----------------------------------------------------------------------------