mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: notifications.data.js
|
||||
* Type of Program: Data
|
||||
* Description: Central registry of all notification types for both admin and
|
||||
* client (user) notifications.
|
||||
*
|
||||
* Each entry 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)
|
||||
* 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, task_submissions_closed, task_submissions_reopened, user_task_overdue, user_task_auto_completed, task_reminder, achievement,
|
||||
* course_unlocked, course_completed, certificate_issued, welcome,
|
||||
* nogrp_welcome, assessment_updated, announcement, tier_expired,
|
||||
* tier_plan_archived, tier_plan_access_revoked, payment_refunded,
|
||||
* task_submission_reviewed, task_assigned, task_completed
|
||||
* Both : broadcast (admin-composed, sent via notification_broadcasts CRUD)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 19, 2026
|
||||
* Date Modified: Jul. 11, 2026 — reverted from notification_templates (DB-editable) back to hardcoded
|
||||
***********************************************************************************************************************************************************************/
|
||||
'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 = [], targetStatus = 'overdue' }) {
|
||||
const label = targetStatus === 'completed' ? 'completed' : 'overdue';
|
||||
return {
|
||||
type: 'task_overdue',
|
||||
title: targetStatus === 'completed' ? 'Tasks Auto-Completed' : 'Tasks Overdue',
|
||||
message: `${count} task${count === 1 ? ' was' : 's were'} automatically marked as ${label}.`,
|
||||
data: { count, task_list_ids, target_status: targetStatus },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── 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, taskId = null }) {
|
||||
return {
|
||||
type: 'task',
|
||||
title: 'Task Updated',
|
||||
message: `The requirements for "${taskName}" have been updated by your administrator.`,
|
||||
data: { taskName, taskListId, groupId, taskId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
task_submissions_closed: {
|
||||
type: 'task',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ taskName, taskListId = null, groupId = null, taskId = null }) {
|
||||
return {
|
||||
type: 'task',
|
||||
title: 'Submissions Closed',
|
||||
message: `"${taskName}" is no longer accepting submissions.`,
|
||||
data: { taskName, taskListId, groupId, taskId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
task_submissions_reopened: {
|
||||
type: 'task',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ taskName, taskListId = null, groupId = null, taskId = null }) {
|
||||
return {
|
||||
type: 'task',
|
||||
title: 'Submissions Reopened',
|
||||
message: `"${taskName}" is accepting submissions again.`,
|
||||
data: { taskName, taskListId, groupId, taskId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
user_task_overdue: {
|
||||
type: 'task',
|
||||
scope: 'user',
|
||||
trigger: 'cron',
|
||||
build({ count, task_list_ids = [], taskListId = null, taskId = null }) {
|
||||
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, taskListId, taskId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
user_task_auto_completed: {
|
||||
type: 'task',
|
||||
scope: 'user',
|
||||
trigger: 'cron',
|
||||
build({ count, task_list_ids = [], taskListId = null, taskId = null }) {
|
||||
const label = count === 1 ? '1 task has' : `${count} tasks have`;
|
||||
return {
|
||||
type: 'task',
|
||||
title: 'Tasks Auto-Completed',
|
||||
message: `${label} passed their deadline and been automatically marked as completed.`,
|
||||
data: { count, task_list_ids, taskListId, taskId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
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 },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── Task submission review (approve/reject) ─────────────────────────────
|
||||
task_submission_reviewed: {
|
||||
type: 'task',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ taskName, status, review_note = null }) {
|
||||
const statusLabel = status === 'approved' ? 'approved' : 'rejected';
|
||||
const reviewNoteSuffix = review_note ? ` Note: ${review_note}` : '';
|
||||
return {
|
||||
type: 'task',
|
||||
title: 'Submission Reviewed',
|
||||
message: `Your submission for "${taskName}" was ${statusLabel}.${reviewNoteSuffix}`,
|
||||
data: { taskName, status, review_note },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── Task list assignment (group added to a task list) ───────────────────
|
||||
task_assigned: {
|
||||
type: 'task',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ taskListName, taskCount }) {
|
||||
return {
|
||||
type: 'task',
|
||||
title: 'New Task Assigned',
|
||||
message: `You have been assigned "${taskListName}" — ${taskCount} task(s) to complete.`,
|
||||
data: { taskListName, taskCount },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── Per-task completion (0→1 transition) ─────────────────────────────────
|
||||
task_completed: {
|
||||
type: 'task',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ taskName }) {
|
||||
return {
|
||||
type: 'task',
|
||||
title: 'Task Completed',
|
||||
message: `You completed "${taskName}".`,
|
||||
data: { taskName },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── Achievement — title/message come from the achievement definition itself ─
|
||||
achievement: {
|
||||
type: 'achievement',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ label, description, key }) {
|
||||
return {
|
||||
type: 'achievement',
|
||||
title: label,
|
||||
message: description,
|
||||
data: { key },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── 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 ──────────────────────────────────────────────────────────────
|
||||
announcement: {
|
||||
type: 'announcement',
|
||||
scope: 'user',
|
||||
trigger: 'manual',
|
||||
build({ title, body }) {
|
||||
return {
|
||||
type: 'announcement',
|
||||
title,
|
||||
message: body,
|
||||
data: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── Broadcast (admin-composed, manual) — title/message typed per-send via
|
||||
// the notification_broadcasts CRUD, not a fixed template ─────────────────
|
||||
broadcast: {
|
||||
type: 'announcement',
|
||||
scope: 'both',
|
||||
trigger: 'manual',
|
||||
build({ title, message, targetType = null, targetId = null, groupId = null, linkUrl = null, linkLabel = null }) {
|
||||
return {
|
||||
type: 'announcement',
|
||||
title,
|
||||
message,
|
||||
data: { targetType, targetId, groupId, linkUrl, linkLabel },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// ── 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 },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
tier_plan_archived: {
|
||||
type: 'tier_plan_archived',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ label, planId = null }) {
|
||||
return {
|
||||
type: 'tier_plan_archived',
|
||||
title: 'Plan Archived',
|
||||
message: `Your "${label}" plan has been archived and is no longer available for new subscriptions. Your current access is unaffected until it expires.`,
|
||||
data: { label, planId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Distinct from tier_plan_archived above — that one explicitly says access
|
||||
// is unaffected, which would be false here, so an admin force-revoke
|
||||
// always fires THIS instead of (never alongside) tier_plan_archived.
|
||||
tier_plan_access_revoked: {
|
||||
type: 'tier_plan_access_revoked',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ label, planId = null }) {
|
||||
return {
|
||||
type: 'tier_plan_access_revoked',
|
||||
title: 'Subscription Access Revoked',
|
||||
message: `Your access under the "${label}" plan has been revoked by an administrator, effective immediately. This subscription is non-refundable.`,
|
||||
data: { label, planId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Fired when a user self-serves a refund within their plan's refund window
|
||||
// (controllers/client/tiers.controller.js refundOrder) — distinct from
|
||||
// tier_plan_access_revoked above, which is an admin-initiated, non-refundable cutoff.
|
||||
payment_refunded: {
|
||||
type: 'payment_refunded',
|
||||
scope: 'user',
|
||||
trigger: 'event',
|
||||
build({ label, amount, currency, planId = null }) {
|
||||
return {
|
||||
type: 'payment_refunded',
|
||||
title: 'Refund Processed',
|
||||
message: `Your refund of ${currency} ${amount} for the "${label}" plan has been processed. Your access to this plan has been revoked.`,
|
||||
data: { label, amount, currency, planId },
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
module.exports = { NOTIFICATION_REGISTRY };
|
||||
Reference in New Issue
Block a user