add: more commits

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-03 21:40:24 +08:00
parent 1d12f04967
commit a572c1e25f
17 changed files with 476 additions and 302 deletions
+3 -3
View File
@@ -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);
}
};
+10 -6
View File
@@ -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,
+30 -37
View File
@@ -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({
groupName: group.name,
groupCode: group.group_code,
userEmail: email,
}),
}).catch(err => console.error('[AUTH] Failed to emit user_registration notification:', err));
renderNotification({ type: 'user_registration', data: {
groupName: group.name,
groupCode: group.group_code,
userEmail: email,
} })
.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({
userEmail: email,
regType: 'system',
}),
}).catch(err => console.error('[AUTH] Failed to emit nogrp_user_registered notification:', err));
renderNotification({ type: 'nogrp_user_registered', data: {
userEmail: email,
regType: 'system',
} })
.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({
userEmail: payload.email,
regType: 'google',
}),
}).catch(err => console.error('[AUTH] googleCallback: Failed to emit nogrp_user_registered notification:', err));
renderNotification({ type: 'nogrp_user_registered', data: {
userEmail: payload.email,
regType: 'google',
} })
.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;
+4 -5
View File
@@ -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.", {
+8 -9
View File
@@ -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({
tier: tier.tier,
label: tier.plan?.label ?? null,
planId: tier.plan?.plan_id ?? null,
}),
}).catch(() => {});
renderNotification({ type: 'tier_expired', data: {
tier: tier.tier,
label: tier.plan?.label ?? null,
planId: tier.plan?.plan_id ?? null,
} })
.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,
});