mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
add: more commits
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@ const { restoreOne, restoreMany } = require("../../utils/courses/restore.util");
|
||||
const { getFieldValues } = require("../../utils/fieldValues.util");
|
||||
const logActivity = require('../../utils/logActivity.util');
|
||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
const { renderNotification } = require('../../services/notificationTemplate.service');
|
||||
|
||||
// ── Models ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1475,11 +1475,11 @@ exports.updateAssessment = async (req, res) => {
|
||||
where: { course_id: courseId },
|
||||
attributes: ['title', 'uuid'],
|
||||
});
|
||||
const notify = NOTIFICATION_REGISTRY.assessment_updated.build({
|
||||
const notify = await renderNotification({ type: 'assessment_updated', data: {
|
||||
assessmentTitle: assessment.title,
|
||||
courseTitle: course?.title ?? null,
|
||||
courseUuid: course?.uuid ?? null,
|
||||
});
|
||||
} });
|
||||
const now = new Date();
|
||||
await UserNotification.bulkCreate(
|
||||
inProgressSessions.map(({ user_id }) => ({
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
const mdl_NotificationTemplate = require('../../models/notifications/notification_template.mdl');
|
||||
const R = require('../../utils/response.util');
|
||||
const logActivity = require('../../utils/logActivity.util');
|
||||
|
||||
// ─── GET /admin/notification-templates ─────────────────────────────────────────
|
||||
|
||||
exports.getNotificationTemplates = async (req, res) => {
|
||||
try {
|
||||
const templates = await mdl_NotificationTemplate.findAll({
|
||||
order: [['notify_type', 'ASC'], ['type', 'ASC']],
|
||||
});
|
||||
return R.success(res, 'Notification templates retrieved.', templates);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET NOTIFICATION TEMPLATES]', err);
|
||||
return R.error(res, 'Could not retrieve notification templates.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── GET /admin/notification-templates/:id ─────────────────────────────────────
|
||||
|
||||
exports.getNotificationTemplate = async (req, res) => {
|
||||
try {
|
||||
const template = await mdl_NotificationTemplate.findByPk(req.params.id);
|
||||
if (!template) return R.error(res, 'Notification template not found.', 404);
|
||||
return R.success(res, 'Notification template retrieved.', template);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET NOTIFICATION TEMPLATE]', err);
|
||||
return R.error(res, 'Could not retrieve notification template.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── PUT /admin/notification-templates/:id ─────────────────────────────────────
|
||||
// No create/delete endpoints — every row is is_system by definition (a new
|
||||
// type needs a code call site before it means anything), so there is nothing
|
||||
// valid to create or delete through this UI.
|
||||
|
||||
exports.updateNotificationTemplate = async (req, res) => {
|
||||
try {
|
||||
const template = await mdl_NotificationTemplate.findByPk(req.params.id);
|
||||
if (!template) return R.error(res, 'Notification template not found.', 404);
|
||||
|
||||
const { label, title, message, publish } = req.body;
|
||||
|
||||
if (title !== undefined && !title.trim()) return R.error(res, 'title cannot be empty.', 400);
|
||||
if (message !== undefined && !message.trim()) return R.error(res, 'message cannot be empty.', 400);
|
||||
|
||||
// "Publish" writes title/message straight to the live columns
|
||||
// renderNotification() reads and clears any pending draft. A plain save
|
||||
// (no publish flag) writes into draft_title/draft_message instead, so
|
||||
// real notifications keep using the last-published content until an
|
||||
// admin comes back and explicitly publishes again.
|
||||
const isPublishing = publish === true || publish === 'true';
|
||||
const nextTitle = title ?? template.draft_title ?? template.title;
|
||||
const nextMessage = message ?? template.draft_message ?? template.message;
|
||||
|
||||
await template.update({
|
||||
label: label ?? template.label,
|
||||
...(isPublishing
|
||||
? {
|
||||
status: 'sent',
|
||||
title: nextTitle,
|
||||
message: nextMessage,
|
||||
draft_title: null,
|
||||
draft_message: null,
|
||||
last_sent_at: new Date(),
|
||||
}
|
||||
: {
|
||||
draft_title: nextTitle,
|
||||
draft_message: nextMessage,
|
||||
}),
|
||||
});
|
||||
|
||||
logActivity(req.user?.user_id, 'update_notification_template', { entityType: 'notification_template', entityId: template.notification_template_id, details: { type: template.type, published: isPublishing } });
|
||||
|
||||
return R.success(res, 'Notification template updated.', template);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][UPDATE NOTIFICATION TEMPLATE]', err);
|
||||
return R.error(res, 'Could not update notification template.', 500);
|
||||
}
|
||||
};
|
||||
@@ -14,7 +14,7 @@ const { Task, TaskList, TaskRequirement, TaskListGroup, mdl_UserGroups } = requi
|
||||
const { mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
|
||||
const mdl_Users = require('../../models/users/users.mdl');
|
||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
const { renderNotification } = require('../../services/notificationTemplate.service');
|
||||
const { adminExclude, jsonbSchemas, computedAttributes } = require('../../models/task/task.attributes');
|
||||
|
||||
const R = require('../../utils/response.util');
|
||||
@@ -652,14 +652,18 @@ exports.updateTask = async (req, res) => {
|
||||
|
||||
if (members.length) {
|
||||
const now = new Date();
|
||||
// Title/message are identical for every member — render once,
|
||||
// then vary only the per-member groupId in the data payload.
|
||||
const notify = await renderNotification({ type: 'task_requirements_updated', data: {
|
||||
taskName: full.name,
|
||||
taskListId: task.task_list_id,
|
||||
groupId: null,
|
||||
} });
|
||||
await UserNotification.bulkCreate(
|
||||
members.map(({ user_id, group_id }) => ({
|
||||
user_id,
|
||||
...NOTIFICATION_REGISTRY.task_requirements_updated.build({
|
||||
taskName: full.name,
|
||||
taskListId: task.task_list_id,
|
||||
groupId: group_id,
|
||||
}),
|
||||
...notify,
|
||||
data: { ...notify.data, groupId: group_id },
|
||||
seen: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
|
||||
@@ -36,7 +36,7 @@ const { generateOTP, getOTPExpiry, isOTPExpired } = require('../utils/otp.util')
|
||||
const { onUserRegistered } = require('../services/achievements.service');
|
||||
const AdminNotification = require('../models/notifications/admin_notification.mdl');
|
||||
const UserNotification = require('../models/notifications/user_notification.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../data/notifications.data');
|
||||
const { renderNotification } = require('../services/notificationTemplate.service');
|
||||
const { sendEmail } = require('../services/email.service');
|
||||
const buildSessionInfo = require('../utils/session_info.util');
|
||||
const logActivity = require('../utils/logActivity.util');
|
||||
@@ -109,20 +109,20 @@ exports.register = async (req, res) => {
|
||||
|
||||
// Fire-and-forget: notify admins — explicit group or NOGRP fallback
|
||||
if (group) {
|
||||
AdminNotification.create({
|
||||
...NOTIFICATION_REGISTRY.user_registration.build({
|
||||
renderNotification({ type: 'user_registration', data: {
|
||||
groupName: group.name,
|
||||
groupCode: group.group_code,
|
||||
userEmail: email,
|
||||
}),
|
||||
}).catch(err => console.error('[AUTH] Failed to emit user_registration notification:', err));
|
||||
} })
|
||||
.then(notify => AdminNotification.create(notify))
|
||||
.catch(err => console.error('[AUTH] Failed to emit user_registration notification:', err));
|
||||
} else if (enrollGroup) {
|
||||
AdminNotification.create({
|
||||
...NOTIFICATION_REGISTRY.nogrp_user_registered.build({
|
||||
renderNotification({ type: 'nogrp_user_registered', data: {
|
||||
userEmail: email,
|
||||
regType: 'system',
|
||||
}),
|
||||
}).catch(err => console.error('[AUTH] Failed to emit nogrp_user_registered notification:', err));
|
||||
} })
|
||||
.then(notify => AdminNotification.create(notify))
|
||||
.catch(err => console.error('[AUTH] Failed to emit nogrp_user_registered notification:', err));
|
||||
}
|
||||
|
||||
return R.success(res, 'Registration successful. Please check your email for the OTP.', {
|
||||
@@ -182,12 +182,12 @@ exports.verifyOTP = async (req, res) => {
|
||||
const notifications = [
|
||||
{
|
||||
user_id: user.user_id,
|
||||
...NOTIFICATION_REGISTRY.welcome.build({
|
||||
...(await renderNotification({ type: 'welcome', data: {
|
||||
groupName: grp?.name ?? null,
|
||||
groupCode: grp?.group_code ?? null,
|
||||
accType: user.acc_type,
|
||||
groupId: membership?.group_id ?? null,
|
||||
}),
|
||||
} })),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
@@ -195,7 +195,7 @@ exports.verifyOTP = async (req, res) => {
|
||||
if (grp?.group_code === 'NOGRP') {
|
||||
notifications.push({
|
||||
user_id: user.user_id,
|
||||
...NOTIFICATION_REGISTRY.nogrp_welcome.build(),
|
||||
...(await renderNotification({ type: 'nogrp_welcome', data: {} })),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
@@ -405,28 +405,21 @@ exports.googleCallback = async (req, res) => {
|
||||
.catch(err => console.error('[AUTH] googleCallback: Failed to grant achievements:', err));
|
||||
|
||||
const _now = new Date();
|
||||
UserNotification.bulkCreate([
|
||||
{
|
||||
user_id: user.user_id,
|
||||
...NOTIFICATION_REGISTRY.welcome.build({ groupName: null, groupCode: 'NOGRP', accType: 'user', groupId: noGrp?.group_id ?? null }),
|
||||
createdAt: _now,
|
||||
updatedAt: _now,
|
||||
},
|
||||
{
|
||||
user_id: user.user_id,
|
||||
...NOTIFICATION_REGISTRY.nogrp_welcome.build(),
|
||||
createdAt: _now,
|
||||
updatedAt: _now,
|
||||
},
|
||||
], { validate: false })
|
||||
Promise.all([
|
||||
renderNotification({ type: 'welcome', data: { groupName: null, groupCode: 'NOGRP', accType: 'user', groupId: noGrp?.group_id ?? null } }),
|
||||
renderNotification({ type: 'nogrp_welcome', data: {} }),
|
||||
]).then(([welcomeNotify, nogrpNotify]) => UserNotification.bulkCreate([
|
||||
{ user_id: user.user_id, ...welcomeNotify, createdAt: _now, updatedAt: _now },
|
||||
{ user_id: user.user_id, ...nogrpNotify, createdAt: _now, updatedAt: _now },
|
||||
], { validate: false }))
|
||||
.catch(err => console.error('[AUTH] googleCallback: Failed to emit welcome notifications:', err));
|
||||
|
||||
AdminNotification.create({
|
||||
...NOTIFICATION_REGISTRY.nogrp_user_registered.build({
|
||||
renderNotification({ type: 'nogrp_user_registered', data: {
|
||||
userEmail: payload.email,
|
||||
regType: 'google',
|
||||
}),
|
||||
}).catch(err => console.error('[AUTH] googleCallback: Failed to emit nogrp_user_registered notification:', err));
|
||||
} })
|
||||
.then(notify => AdminNotification.create(notify))
|
||||
.catch(err => console.error('[AUTH] googleCallback: Failed to emit nogrp_user_registered notification:', err));
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
throw err;
|
||||
|
||||
@@ -35,7 +35,7 @@ const { onCourseCompleted } = require('../../services/achievements.service'
|
||||
const PendingCertificate = require('../../models/courses/pending_certificate.mdl');
|
||||
const Certificate = require('../../models/courses/certificate.mdl');
|
||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
const { renderNotification } = require('../../services/notificationTemplate.service');
|
||||
|
||||
const notDeleted = { deletedAt: null };
|
||||
|
||||
@@ -956,10 +956,9 @@ exports.submitCourseAssessment = async (req, res) => {
|
||||
}
|
||||
|
||||
// Immediate notification: course completed, certificate incoming
|
||||
UserNotification.create({
|
||||
user_id,
|
||||
...NOTIFICATION_REGISTRY.course_completed.build({ courseTitle: course?.title ?? '', courseUuid: course?.uuid ?? null }),
|
||||
}).catch(err => console.error('[ASSESSMENT] Failed to emit course_completed notification:', err));
|
||||
renderNotification({ type: 'course_completed', data: { courseTitle: course?.title ?? '', courseUuid: course?.uuid ?? null } })
|
||||
.then(notify => UserNotification.create({ user_id, ...notify }))
|
||||
.catch(err => console.error('[ASSESSMENT] Failed to emit course_completed notification:', err));
|
||||
}
|
||||
|
||||
return R.success(res, "Assessment submitted.", {
|
||||
|
||||
@@ -22,7 +22,7 @@ const { onTierActivated } = require('../../services/achievements.service');
|
||||
const { Course } = require('../../models/courses/courses.mdl');
|
||||
const paymentSvc = require('../../services/payment.service');
|
||||
const R = require('../../utils/response.util');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
const { renderNotification } = require('../../services/notificationTemplate.service');
|
||||
|
||||
require('../../models/tiers/tier.associations');
|
||||
|
||||
@@ -49,14 +49,13 @@ exports.getMyTier = async (req, res) => {
|
||||
// ── Inline safety net: expire between cron ticks ──────────────────────────
|
||||
if (tier && tier.expires_at && new Date(tier.expires_at) <= new Date()) {
|
||||
await tier.update({ status: 'expired' });
|
||||
UserNotification.create({
|
||||
user_id: req.user.user_id,
|
||||
...NOTIFICATION_REGISTRY.tier_expired.build({
|
||||
renderNotification({ type: 'tier_expired', data: {
|
||||
tier: tier.tier,
|
||||
label: tier.plan?.label ?? null,
|
||||
planId: tier.plan?.plan_id ?? null,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
} })
|
||||
.then(notify => UserNotification.create({ user_id: req.user.user_id, ...notify }))
|
||||
.catch(() => {});
|
||||
return R.success(res, 'Active tier retrieved.', {
|
||||
tier: 'free', status: 'active', category: null, just_expired: true,
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ const mdl_UserTiers = require('../../models/tiers/user_tiers.mdl');
|
||||
const mdl_TierPlans = require('../../models/tiers/tier_plans.mdl');
|
||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||
const CronNotificationSetting = require('../../models/notifications/cron_notification_setting.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
const { getNotificationTemplate, renderNotificationContent } = require('../../services/notificationTemplate.service');
|
||||
|
||||
require('../../models/tiers/tier.associations');
|
||||
|
||||
@@ -73,18 +73,17 @@ async function run() {
|
||||
// Status flip above always happens — only this step is skippable via settings.
|
||||
const settings = await CronNotificationSetting.findOne({ where: { job_name: 'expireUserTiers' } });
|
||||
if (!settings || settings.enabled) {
|
||||
const notifications = expired.map((t) =>
|
||||
NOTIFICATION_REGISTRY.tier_expired.build({
|
||||
try {
|
||||
const template = await getNotificationTemplate('tier_expired');
|
||||
const notifications = expired.map((t) => ({
|
||||
user_id: t.user_id,
|
||||
...renderNotificationContent(template, {
|
||||
tier: t.tier,
|
||||
label: t.plan?.label ?? null,
|
||||
planId: t.plan?.plan_id ?? null,
|
||||
})
|
||||
).map((payload, i) => ({
|
||||
user_id: expired[i].user_id,
|
||||
...payload,
|
||||
}),
|
||||
}));
|
||||
|
||||
try {
|
||||
await UserNotification.bulkCreate(notifications, { ignoreDuplicates: true });
|
||||
} catch (err) {
|
||||
console.error('[CRON][EXPIRE TIERS] Notification bulkCreate failed:', err);
|
||||
|
||||
@@ -32,7 +32,7 @@ const PendingCertificate = require('../../models/courses/pending_certificate.mdl
|
||||
const mdl_Achievements = require('../../models/users/achievements.mdl');
|
||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||
const CronNotificationSetting = require('../../models/notifications/cron_notification_setting.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
const { getNotificationTemplate, renderNotificationContent } = require('../../services/notificationTemplate.service');
|
||||
const { ensureCertificateRecord } = require('../../services/certificate-record.service');
|
||||
|
||||
async function run() {
|
||||
@@ -59,6 +59,10 @@ async function run() {
|
||||
|
||||
console.log(`[CRON][ISSUE CERTS] Processing ${rows.length} pending certificate(s).`);
|
||||
|
||||
// Fetched once outside the loop — content differs per row (courseTitle),
|
||||
// but there's no need to re-query the template for every row.
|
||||
const certificateTemplate = notificationsEnabled ? await getNotificationTemplate('certificate_issued') : null;
|
||||
|
||||
for (const row of rows) {
|
||||
const { pending_id, user_id, course_uuid, course_title } = row;
|
||||
const achKey = `course_completed_${course_uuid}`;
|
||||
@@ -87,7 +91,7 @@ async function run() {
|
||||
if (notificationsEnabled) {
|
||||
await UserNotification.create({
|
||||
user_id,
|
||||
...NOTIFICATION_REGISTRY.certificate_issued.build({
|
||||
...renderNotificationContent(certificateTemplate, {
|
||||
courseTitle: course_title ?? '',
|
||||
courseUuid: course_uuid,
|
||||
}),
|
||||
|
||||
@@ -26,7 +26,7 @@ const { Op } = require('sequelize');
|
||||
const { Task } = require('../../models/task/task.mdl');
|
||||
const AdminNotification = require('../../models/notifications/admin_notification.mdl');
|
||||
const CronNotificationSetting = require('../../models/notifications/cron_notification_setting.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
const { renderNotification } = require('../../services/notificationTemplate.service');
|
||||
|
||||
// ─── The actual sweep ────────────────────────────────────────────────────────
|
||||
async function run() {
|
||||
@@ -59,7 +59,7 @@ async function run() {
|
||||
if (settings && !settings.enabled) return;
|
||||
|
||||
await AdminNotification.create(
|
||||
NOTIFICATION_REGISTRY.task_overdue.build({ count: affectedCount })
|
||||
await renderNotification({ type: 'task_overdue', data: { count: affectedCount } })
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[CRON][TASK OVERDUE] Failed to insert admin notification:', err);
|
||||
|
||||
@@ -25,7 +25,7 @@ const sequelize = require('../../config/db.config');
|
||||
const { Task } = require('../../models/task/task.mdl');
|
||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||
const CronNotificationSetting = require('../../models/notifications/cron_notification_setting.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
const { renderNotification } = require('../../services/notificationTemplate.service');
|
||||
|
||||
const WINDOW_MS = 65 * 60 * 1000; // 65-minute lookback
|
||||
|
||||
@@ -71,7 +71,7 @@ async function run() {
|
||||
|
||||
const count = recentlyOverdue.length;
|
||||
const now = new Date();
|
||||
const notify = NOTIFICATION_REGISTRY.user_task_overdue.build({ count, task_list_ids: taskListIds });
|
||||
const notify = await renderNotification({ type: 'user_task_overdue', data: { count, task_list_ids: taskListIds } });
|
||||
|
||||
await UserNotification.bulkCreate(
|
||||
affectedUsers.map(({ user_id }) => ({
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: notification_template_enrichers.data.js
|
||||
* Type of Program: Data / Registry
|
||||
* Description: Admin-edited notification templates are plain text — no
|
||||
* conditionals or expressions allowed. Any type that used to
|
||||
* branch on data in JS (e.g. task_overdue's singular/plural
|
||||
* wording) gets that branch precomputed here into flat
|
||||
* placeholder keys BEFORE substitution, so the stored title/
|
||||
* message only ever needs straight {{key}} swaps.
|
||||
* Mirrors data/email_template_enrichers.data.js.
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 3, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { fmtDate } = require('../utils/datetime.util');
|
||||
|
||||
const ENRICHERS = {
|
||||
task_overdue: (data) => ({
|
||||
...data,
|
||||
task_word: Number(data.count) === 1 ? 'task was' : 'tasks were',
|
||||
}),
|
||||
|
||||
user_task_overdue: (data) => ({
|
||||
...data,
|
||||
task_label: Number(data.count) === 1 ? '1 task has' : `${data.count} tasks have`,
|
||||
}),
|
||||
|
||||
task_reminder: (data) => ({
|
||||
...data,
|
||||
deadline: fmtDate(data.deadline),
|
||||
}),
|
||||
|
||||
welcome: (data) => {
|
||||
const greeting = data.accType === 'admin'
|
||||
? 'Welcome, Administrator!'
|
||||
: data.accType === 'staff'
|
||||
? 'Welcome to the Philproperties team!'
|
||||
: 'Welcome to Philproperties!';
|
||||
return {
|
||||
...data,
|
||||
greeting,
|
||||
group_suffix: data.groupName ? ` You have been added to ${data.groupName}.` : '',
|
||||
};
|
||||
},
|
||||
|
||||
assessment_updated: (data) => ({
|
||||
...data,
|
||||
assessmentTitle: data.assessmentTitle || 'Course Assessment',
|
||||
courseTitle: data.courseTitle || 'your course',
|
||||
}),
|
||||
|
||||
tier_expired: (data) => ({
|
||||
...data,
|
||||
planLabel: data.label ?? data.tier,
|
||||
}),
|
||||
};
|
||||
|
||||
const enrichNotificationData = (type, data = {}) => (ENRICHERS[type] ? ENRICHERS[type](data) : data);
|
||||
|
||||
module.exports = { enrichNotificationData };
|
||||
+18
-224
@@ -1,137 +1,38 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: notifications.data.js
|
||||
* Type of Program: Data
|
||||
* Description: Central registry of all notification types for both admin and
|
||||
* client (user) notifications.
|
||||
* Description: Registry of notification types that have no fixed, admin-
|
||||
* editable wording — content is entirely supplied by the caller
|
||||
* at trigger time, so there's nothing to template.
|
||||
*
|
||||
* Each entry describes one notification type:
|
||||
* Every other system-triggered notification type (task overdue,
|
||||
* welcome, tier expired, etc.) has been moved to the
|
||||
* notification_templates table — admin-editable, {{placeholder}}-
|
||||
* based, rendered via services/notificationTemplate.service.js's
|
||||
* renderNotification()/renderNotificationContent(). See
|
||||
* controllers/admin/notification_templates.controller.js.
|
||||
*
|
||||
* Each entry here describes one notification type:
|
||||
* type {string} — stored in the DB 'type' column
|
||||
* scope {string} — 'admin' | 'user' | 'both'
|
||||
* trigger {string} — what fires it (cron | event | manual)
|
||||
* trigger {string} — what fires it (event | manual)
|
||||
* build {function} — takes a data payload, returns the object
|
||||
* ready to pass to AdminNotification.create()
|
||||
* or UserNotification.create() / bulkCreate()
|
||||
*
|
||||
* To add a new notification type:
|
||||
* 1. Add an entry in the relevant section below.
|
||||
* 2. Call NOTIFICATION_REGISTRY.<key>.build(data) at the trigger
|
||||
* site (controller, cron, service).
|
||||
* No other changes needed.
|
||||
*
|
||||
* Current types:
|
||||
* Admin : task_overdue, user_registration, nogrp_user_registered
|
||||
* User : task_requirements_updated, user_task_overdue, achievement, course_unlocked,
|
||||
* course_completed, certificate_issued, task_reminder, announcement,
|
||||
* nogrp_welcome, tier_expired
|
||||
* User : achievement, announcement
|
||||
* Both : broadcast (admin-composed, sent via notification_broadcasts CRUD)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 19, 2026
|
||||
* Date Modified: Jul. 3, 2026 — fixed-wording types moved into notification_templates
|
||||
***********************************************************************************************************************************************************************/
|
||||
'use strict';
|
||||
|
||||
const { fmtDate } = require('../utils/datetime.util');
|
||||
|
||||
const NOTIFICATION_REGISTRY = {
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// ADMIN notifications (scope: 'admin')
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── Task ──────────────────────────────────────────────────────────────────
|
||||
task_overdue: {
|
||||
type: 'task_overdue',
|
||||
scope: 'admin',
|
||||
trigger: 'cron',
|
||||
build({ count, task_list_ids = [] }) {
|
||||
return {
|
||||
type: 'task_overdue',
|
||||
title: 'Tasks Overdue',
|
||||
message: `${count} task${count === 1 ? ' was' : 's were'} automatically marked as overdue.`,
|
||||
data: { count, task_list_ids },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── User Registration ─────────────────────────────────────────────────────
|
||||
user_registration: {
|
||||
type: 'user_registration',
|
||||
scope: 'admin',
|
||||
trigger: 'event',
|
||||
build({ groupName, groupCode, userEmail }) {
|
||||
return {
|
||||
type: 'user_registration',
|
||||
title: 'New User Registered',
|
||||
message: `A new user registered in ${groupName}.`,
|
||||
data: { groupName, groupCode, userEmail },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── Unaffiliated User Registration ───────────────────────────────────────
|
||||
nogrp_user_registered: {
|
||||
type: 'nogrp_user_registered',
|
||||
scope: 'admin',
|
||||
trigger: 'event',
|
||||
build({ userEmail, regType }) {
|
||||
return {
|
||||
type: 'nogrp_user_registered',
|
||||
title: 'New Unaffiliated User',
|
||||
message: `A new user (${userEmail}) registered via ${regType} without a group code and was placed in the default group.`,
|
||||
data: { userEmail, regType },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// USER notifications (scope: 'user')
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── Task ──────────────────────────────────────────────────────────────────
|
||||
task_requirements_updated: {
|
||||
type: 'task',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ taskName, taskListId = null, groupId = null }) {
|
||||
return {
|
||||
type: 'task',
|
||||
title: 'Task Updated',
|
||||
message: `The requirements for "${taskName}" have been updated by your administrator.`,
|
||||
data: { taskName, taskListId, groupId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
user_task_overdue: {
|
||||
type: 'task',
|
||||
scope: 'user',
|
||||
trigger: 'cron',
|
||||
build({ count, task_list_ids = [] }) {
|
||||
const label = count === 1 ? '1 task has' : `${count} tasks have`;
|
||||
return {
|
||||
type: 'task',
|
||||
title: 'Tasks Overdue',
|
||||
message: `${label} passed their deadline and been marked as overdue.`,
|
||||
data: { count, task_list_ids },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
task_reminder: {
|
||||
type: 'task',
|
||||
scope: 'user',
|
||||
trigger: 'cron',
|
||||
build({ taskName, deadline, taskListId = null, groupId = null }) {
|
||||
return {
|
||||
type: 'task',
|
||||
title: 'Task Deadline Approaching',
|
||||
message: `"${taskName}" is due on ${fmtDate(deadline)}.`,
|
||||
data: { taskName, deadline, taskListId, groupId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── Achievement ───────────────────────────────────────────────────────────
|
||||
// ── Achievement — title/message come from the achievement definition itself ─
|
||||
achievement: {
|
||||
type: 'achievement',
|
||||
scope: 'user',
|
||||
@@ -146,100 +47,7 @@ const NOTIFICATION_REGISTRY = {
|
||||
},
|
||||
},
|
||||
|
||||
// ── Course ────────────────────────────────────────────────────────────────
|
||||
course_unlocked: {
|
||||
type: 'course',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ courseTitle, courseUuid = null }) {
|
||||
return {
|
||||
type: 'course',
|
||||
title: 'New Course Available',
|
||||
message: `"${courseTitle}" has been added to your learning library.`,
|
||||
data: { courseTitle, courseUuid },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
course_completed: {
|
||||
type: 'course',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ courseTitle, courseUuid = null }) {
|
||||
return {
|
||||
type: 'course',
|
||||
title: 'Course Completed',
|
||||
message: `Great job! You've completed "${courseTitle}". Your certificate will be issued within the next hour.`,
|
||||
data: { courseTitle, courseUuid },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
certificate_issued: {
|
||||
type: 'course',
|
||||
scope: 'user',
|
||||
trigger: 'cron',
|
||||
build({ courseTitle, courseUuid }) {
|
||||
return {
|
||||
type: 'course',
|
||||
title: 'Certificate Issued',
|
||||
message: `Congratulations! Your certificate for "${courseTitle}" is ready.`,
|
||||
data: { courseTitle, courseUuid },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── Welcome ───────────────────────────────────────────────────────────────
|
||||
welcome: {
|
||||
type: 'announcement',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ groupName, groupCode, accType, groupId = null }) {
|
||||
const greeting = accType === 'admin'
|
||||
? 'Welcome, Administrator!'
|
||||
: accType === 'staff'
|
||||
? 'Welcome to the Philproperties team!'
|
||||
: 'Welcome to Philproperties!';
|
||||
return {
|
||||
type: 'announcement',
|
||||
title: 'Welcome to Philproperties',
|
||||
message: groupName ? `${greeting} You have been added to ${groupName}.` : greeting,
|
||||
data: { groupName, groupCode, accType, groupId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── No-Group Welcome ──────────────────────────────────────────────────────
|
||||
nogrp_welcome: {
|
||||
type: 'announcement',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build() {
|
||||
return {
|
||||
type: 'announcement',
|
||||
title: "You're Not in a Group Yet",
|
||||
message: 'You are currently in the default group. Contact an administrator to be assigned to your team.',
|
||||
data: { groupCode: 'NOGRP' },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── Assessment ────────────────────────────────────────────────────────────
|
||||
assessment_updated: {
|
||||
type: 'assessment',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ assessmentTitle, courseTitle, courseUuid = null }) {
|
||||
return {
|
||||
type: 'assessment',
|
||||
title: 'Assessment Updated',
|
||||
message: `The administrator has updated the "${assessmentTitle || 'Course Assessment'}" in "${courseTitle || 'your course'}". Your current session is still valid — continue where you left off.`,
|
||||
data: { assessmentTitle, courseTitle, courseUuid },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── Platform ──────────────────────────────────────────────────────────────
|
||||
// ── Platform — title/body typed fresh by whoever calls this ─────────────────
|
||||
announcement: {
|
||||
type: 'announcement',
|
||||
scope: 'user',
|
||||
@@ -254,7 +62,8 @@ const NOTIFICATION_REGISTRY = {
|
||||
},
|
||||
},
|
||||
|
||||
// ── Broadcast (admin-composed, manual) ───────────────────────────────────
|
||||
// ── Broadcast (admin-composed, manual) — title/message typed per-send via
|
||||
// the notification_broadcasts CRUD, not a fixed template ─────────────────
|
||||
broadcast: {
|
||||
type: 'announcement',
|
||||
scope: 'both',
|
||||
@@ -269,21 +78,6 @@ const NOTIFICATION_REGISTRY = {
|
||||
},
|
||||
},
|
||||
|
||||
// ── Tier ──────────────────────────────────────────────────────────────────
|
||||
tier_expired: {
|
||||
type: 'tier_expired',
|
||||
scope: 'user',
|
||||
trigger: 'cron',
|
||||
build({ tier, label, planId = null }) {
|
||||
return {
|
||||
type: 'tier_expired',
|
||||
title: 'Subscription Expired',
|
||||
message: `Your ${label ?? tier} plan has expired. Renew to keep access.`,
|
||||
data: { tier, label, planId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
module.exports = { NOTIFICATION_REGISTRY };
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('notification_templates', {
|
||||
notification_template_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
type: { type: Sequelize.STRING(100), allowNull: false, unique: true },
|
||||
notify_type: { type: Sequelize.STRING(64), allowNull: false },
|
||||
scope: { type: Sequelize.ENUM('admin', 'user', 'both'), allowNull: false },
|
||||
label: { type: Sequelize.STRING(150), allowNull: false },
|
||||
status: { type: Sequelize.ENUM('draft', 'sent'), allowNull: false, defaultValue: 'draft' },
|
||||
title: { type: Sequelize.STRING(255), allowNull: true },
|
||||
message: { type: Sequelize.TEXT, allowNull: true },
|
||||
draft_title: { type: Sequelize.STRING(255), allowNull: true },
|
||||
draft_message: { type: Sequelize.TEXT, allowNull: true },
|
||||
last_sent_at: { type: Sequelize.DATE, allowNull: true },
|
||||
is_system: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Ported straight from data/notifications.data.js's NOTIFICATION_REGISTRY —
|
||||
// seeded already-published (status: 'sent', content in the live title/message
|
||||
// columns) because these types fire in production right now via cron/event
|
||||
// triggers. Seeding them as drafts would make every trigger site throw
|
||||
// "no published version yet" the moment this migration lands.
|
||||
await queryInterface.bulkInsert('notification_templates', [
|
||||
{
|
||||
type: 'task_overdue', notify_type: 'task_overdue', scope: 'admin', is_system: true,
|
||||
label: 'Tasks Overdue (Admin)', status: 'sent',
|
||||
title: 'Tasks Overdue',
|
||||
message: '{{count}} {{task_word}} automatically marked as overdue.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'user_registration', notify_type: 'user_registration', scope: 'admin', is_system: true,
|
||||
label: 'New User Registered', status: 'sent',
|
||||
title: 'New User Registered',
|
||||
message: 'A new user registered in {{groupName}}.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'nogrp_user_registered', notify_type: 'nogrp_user_registered', scope: 'admin', is_system: true,
|
||||
label: 'New Unaffiliated User', status: 'sent',
|
||||
title: 'New Unaffiliated User',
|
||||
message: 'A new user ({{userEmail}}) registered via {{regType}} without a group code and was placed in the default group.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'task_requirements_updated', notify_type: 'task', scope: 'user', is_system: true,
|
||||
label: 'Task Requirements Updated', status: 'sent',
|
||||
title: 'Task Updated',
|
||||
message: 'The requirements for "{{taskName}}" have been updated by your administrator.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'user_task_overdue', notify_type: 'task', scope: 'user', is_system: true,
|
||||
label: 'Tasks Overdue (User)', status: 'sent',
|
||||
title: 'Tasks Overdue',
|
||||
message: '{{task_label}} passed their deadline and been marked as overdue.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'task_reminder', notify_type: 'task', scope: 'user', is_system: true,
|
||||
label: 'Task Deadline Approaching', status: 'sent',
|
||||
title: 'Task Deadline Approaching',
|
||||
message: '"{{taskName}}" is due on {{deadline}}.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'course_unlocked', notify_type: 'course', scope: 'user', is_system: true,
|
||||
label: 'New Course Available', status: 'sent',
|
||||
title: 'New Course Available',
|
||||
message: '"{{courseTitle}}" has been added to your learning library.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'course_completed', notify_type: 'course', scope: 'user', is_system: true,
|
||||
label: 'Course Completed', status: 'sent',
|
||||
title: 'Course Completed',
|
||||
message: 'Great job! You\'ve completed "{{courseTitle}}". Your certificate will be issued within the next hour.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'certificate_issued', notify_type: 'course', scope: 'user', is_system: true,
|
||||
label: 'Certificate Issued', status: 'sent',
|
||||
title: 'Certificate Issued',
|
||||
message: 'Congratulations! Your certificate for "{{courseTitle}}" is ready.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'welcome', notify_type: 'announcement', scope: 'user', is_system: true,
|
||||
label: 'Welcome Message', status: 'sent',
|
||||
title: 'Welcome to Philproperties',
|
||||
message: '{{greeting}}{{group_suffix}}',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'nogrp_welcome', notify_type: 'announcement', scope: 'user', is_system: true,
|
||||
label: "Not in a Group Yet", status: 'sent',
|
||||
title: "You're Not in a Group Yet",
|
||||
message: 'You are currently in the default group. Contact an administrator to be assigned to your team.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'assessment_updated', notify_type: 'assessment', scope: 'user', is_system: true,
|
||||
label: 'Assessment Updated', status: 'sent',
|
||||
title: 'Assessment Updated',
|
||||
message: 'The administrator has updated the "{{assessmentTitle}}" in "{{courseTitle}}". Your current session is still valid — continue where you left off.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'tier_expired', notify_type: 'tier_expired', scope: 'user', is_system: true,
|
||||
label: 'Subscription Expired', status: 'sent',
|
||||
title: 'Subscription Expired',
|
||||
message: 'Your {{planLabel}} plan has expired. Renew to keep access.',
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('notification_templates');
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_notification_templates_scope";`);
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_notification_templates_status";`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: notification_template.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Admin-managed catalog of system-triggered notification wording
|
||||
* (title + message). Mirrors models/email_templates/email_templates.mdl.js.
|
||||
* `is_system` rows are the built-in types referenced by `type` from
|
||||
* services/notificationTemplate.service.js's renderNotification()
|
||||
* callers (cron jobs, controllers) — protected from deletion by the
|
||||
* admin controller (no create/delete endpoint at all, since a new
|
||||
* type needs a code call site before it means anything).
|
||||
*
|
||||
* Publish workflow: `title`/`message` are the LIVE content — the
|
||||
* only columns renderNotification() ever reads. Editing a 'sent'
|
||||
* template writes to `draft_title`/`draft_message` instead, leaving
|
||||
* live content untouched until an admin explicitly publishes again
|
||||
* (see controllers/admin/notification_templates.controller.js).
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 3, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
|
||||
const mdl_NotificationTemplate = sequelize.define('NotificationTemplate', {
|
||||
notification_template_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'ID', hidden: true, order: 0 },
|
||||
type: { type: DataTypes.STRING(100), allowNull: false, unique: true, label: 'Type', hidden: false, order: 1, filterable: true },
|
||||
// Written into the delivered admin_notifications/user_notifications row's own
|
||||
// `type` column (e.g. 'task', 'course', 'announcement') — distinct from the
|
||||
// lookup key above, since several lookup keys share the same delivered type.
|
||||
notify_type: { type: DataTypes.STRING(64), allowNull: false, label: 'Notify Type', hidden: false, order: 2, filterable: true },
|
||||
scope: { type: DataTypes.ENUM('admin', 'user', 'both'), allowNull: false, label: 'Scope', hidden: false, order: 3, filterable: true },
|
||||
label: { type: DataTypes.STRING(150), allowNull: false, label: 'Label', hidden: false, order: 4, filterable: true },
|
||||
status: { type: DataTypes.ENUM('draft', 'sent'), allowNull: false, defaultValue: 'draft', label: 'Status', hidden: false, order: 5, filterable: true },
|
||||
title: { type: DataTypes.STRING(255), allowNull: true, label: 'Title', hidden: false, order: 6, filterable: false },
|
||||
message: { type: DataTypes.TEXT, allowNull: true, label: 'Message', hidden: false, order: 7, filterable: false },
|
||||
draft_title: { type: DataTypes.STRING(255), allowNull: true, label: 'Draft Title', hidden: false, order: 8, filterable: false },
|
||||
draft_message: { type: DataTypes.TEXT, allowNull: true, label: 'Draft Message', hidden: false, order: 9, filterable: false },
|
||||
last_sent_at: { type: DataTypes.DATE, allowNull: true, label: 'Last Published', hidden: false, order: 10, filterable: false },
|
||||
is_system: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, label: 'System', hidden: false, order: 11, filterable: true },
|
||||
}, {
|
||||
tableName: 'notification_templates',
|
||||
timestamps: true,
|
||||
paranoid: false,
|
||||
});
|
||||
|
||||
module.exports = mdl_NotificationTemplate;
|
||||
@@ -43,6 +43,7 @@ const mediaRoutes = require('./media.routes');
|
||||
const achievementsRoutes = require('./achievements.routes');
|
||||
const emailTemplatesRoutes = require('./email_templates.routes');
|
||||
const emailBroadcastsRoutes = require('./email_broadcasts.routes');
|
||||
const notificationTemplatesRoutes = require('./notification_templates.routes');
|
||||
const activityCtrl = require('../../controllers/admin/user_activity.controller');
|
||||
|
||||
// ── Guards — applied to ALL admin routes ──────────────────────────────────────
|
||||
@@ -65,6 +66,7 @@ router.use('/advertisements', advertisementRoutes);
|
||||
router.use('/notifications', notificationRoutes);
|
||||
router.use('/notification-broadcasts', notificationBroadcastRoutes);
|
||||
router.use('/notification-settings', notificationSettingsRoutes);
|
||||
router.use('/notification-templates', notificationTemplatesRoutes);
|
||||
router.use('/media', mediaRoutes);
|
||||
router.use('/achievements', achievementsRoutes);
|
||||
router.use('/email-templates', emailTemplatesRoutes);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const ctrl = require('../../controllers/admin/notification_templates.controller');
|
||||
|
||||
// Auth + requireAdmin applied by admin.routes.js
|
||||
|
||||
router.get('/', ctrl.getNotificationTemplates);
|
||||
router.get('/:id', ctrl.getNotificationTemplate);
|
||||
router.put('/:id', ctrl.updateNotificationTemplate);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,52 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: notificationTemplate.service.js
|
||||
* Type of Program: Service
|
||||
* Description: Renders system-triggered notification title/message from the
|
||||
* notification_templates table (admin-editable, see
|
||||
* controllers/admin/notification_templates.controller.js).
|
||||
* Mirrors the template-loading half of services/email.service.js's
|
||||
* sendEmail() — same "only live columns count as published" rule,
|
||||
* same {{placeholder}} substitution via utils/renderTemplate.util.js.
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 3, 2026
|
||||
***********************************************************************************************************************************************************************
|
||||
* HOW TO USE:
|
||||
* const { renderNotification } = require('../services/notificationTemplate.service');
|
||||
* const notify = await renderNotification({ type: 'task_overdue', data: { count } });
|
||||
* await AdminNotification.create(notify);
|
||||
*
|
||||
* // Bulk/loop use — fetch once, render per-row without re-querying the DB:
|
||||
* const { getNotificationTemplate, renderNotificationContent } = require('../services/notificationTemplate.service');
|
||||
* const template = await getNotificationTemplate('tier_expired');
|
||||
* const rows = expired.map((t) => ({ user_id: t.user_id, ...renderNotificationContent(template, { tier: t.tier, label: t.plan?.label ?? null }) }));
|
||||
***********************************************************************************************************************************************************************/
|
||||
const mdl_NotificationTemplate = require('../models/notifications/notification_template.mdl');
|
||||
const { enrichNotificationData } = require('../data/notification_template_enrichers.data');
|
||||
const { renderTemplate } = require('../utils/renderTemplate.util');
|
||||
|
||||
const getNotificationTemplate = async (type) => {
|
||||
const template = await mdl_NotificationTemplate.findOne({ where: { type } });
|
||||
if (!template || !template.title || !template.message) {
|
||||
throw new Error(`Notification template "${type}" has no published version yet`);
|
||||
}
|
||||
return template;
|
||||
};
|
||||
|
||||
// Pure — no DB access. Use when a template has already been fetched once
|
||||
// (e.g. reused across a bulk-create loop) to avoid a query per row.
|
||||
const renderNotificationContent = (template, data = {}) => {
|
||||
const enriched = enrichNotificationData(template.type, data);
|
||||
return {
|
||||
type: template.notify_type,
|
||||
title: renderTemplate(template.title, enriched),
|
||||
message: renderTemplate(template.message, enriched),
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
const renderNotification = async ({ type, data = {} }) => {
|
||||
const template = await getNotificationTemplate(type);
|
||||
return renderNotificationContent(template, data);
|
||||
};
|
||||
|
||||
module.exports = { getNotificationTemplate, renderNotificationContent, renderNotification };
|
||||
Reference in New Issue
Block a user