mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
154 lines
6.5 KiB
JavaScript
154 lines
6.5 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* File Name: audienceResolver.util.js
|
|
* Type of Program: Utility
|
|
* Description: Shared "who does this target reach" resolution for anything
|
|
* broadcast-shaped (notification broadcasts, email broadcasts).
|
|
* Extracted out of controllers/admin/notificationBroadcasts.controller.js
|
|
* so both features resolve task_list/course/tier_plan targeting
|
|
* identically instead of drifting apart.
|
|
* Author: Kenneth Obsequio (@lash0000)
|
|
* Date Created: Jul. 3, 2026
|
|
***********************************************************************************************************************************************************************/
|
|
const { Op, QueryTypes } = require('sequelize');
|
|
const sequelize = require('../config/db.config');
|
|
const mdl_Users = require('../models/users/users.mdl');
|
|
const { TaskList } = require('../models/task/task.mdl');
|
|
const { Course } = require('../models/courses/courses.mdl');
|
|
const mdl_TierCategories = require('../models/tiers/tier_categories.mdl');
|
|
const mdl_UserTiers = require('../models/tiers/user_tiers.mdl');
|
|
const mdl_TierPlans = require('../models/tiers/tier_plans.mdl');
|
|
const mdl_Product = require('../models/courses/products.mdl');
|
|
const mdl_CoursePurchase = require('../models/courses/course_purchases.mdl');
|
|
|
|
const ALLOWED_TARGET_TYPES = ["admin", "user", "both", "task_list", "course", "tier_plan"];
|
|
const SCOPED_TARGET_TYPES = ["task_list", "course", "tier_plan"];
|
|
|
|
async function validateTargetId(target_type, target_id) {
|
|
if (target_type === "task_list") {
|
|
const row = await TaskList.findOne({ where: { task_list_id: target_id, deletedAt: null } });
|
|
if (!row) { const err = new Error("Selected task list was not found."); err.status = 400; throw err; }
|
|
} else if (target_type === "course") {
|
|
const row = await Course.findOne({ where: { uuid: target_id, deletedAt: null } });
|
|
if (!row) { const err = new Error("Selected course was not found."); err.status = 400; throw err; }
|
|
} else if (target_type === "tier_plan") {
|
|
const row = await mdl_TierPlans.findOne({ where: { plan_id: target_id, deletedAt: null } });
|
|
if (!row) { const err = new Error("Selected tier plan was not found."); err.status = 400; throw err; }
|
|
}
|
|
}
|
|
|
|
// Rank-0 (free) courses resolve like target_type: 'user' — everyone qualifies.
|
|
async function resolveCourseUserIds(courseUuid) {
|
|
const course = await Course.findOne({ where: { uuid: courseUuid, deletedAt: null }, attributes: ['course_id', 'subscription'] });
|
|
if (!course) return [];
|
|
|
|
// rank is BIGINT on CockroachDB — Sequelize returns it as a string, so normalize to Number.
|
|
const categories = await mdl_TierCategories.findAll({ attributes: ['slug', 'rank'] });
|
|
const rankMap = Object.fromEntries(categories.map((c) => [c.slug, Number(c.rank)]));
|
|
const courseRank = rankMap[course.subscription] ?? Infinity;
|
|
|
|
const userIds = new Set();
|
|
|
|
if (courseRank === 0) {
|
|
const users = await mdl_Users.findAll({ attributes: ['user_id'], where: { acc_type: 'user', deletedAt: null }, raw: true });
|
|
users.forEach((u) => userIds.add(String(u.user_id)));
|
|
return [...userIds];
|
|
}
|
|
|
|
const qualifyingSlugs = Object.entries(rankMap).filter(([, rank]) => rank >= courseRank).map(([slug]) => slug);
|
|
if (qualifyingSlugs.length) {
|
|
const holders = await mdl_UserTiers.findAll({
|
|
attributes: ['user_id'],
|
|
where: { status: 'active', tier: { [Op.in]: qualifyingSlugs } },
|
|
raw: true,
|
|
});
|
|
holders.forEach((h) => userIds.add(String(h.user_id)));
|
|
}
|
|
|
|
const product = await mdl_Product.findOne({ where: { course_id: course.course_id } });
|
|
if (product) {
|
|
const purchasers = await mdl_CoursePurchase.findAll({
|
|
attributes: ['user_id'],
|
|
where: {
|
|
product_id: product.id,
|
|
status: 'completed',
|
|
[Op.or]: [{ expires_at: null }, { expires_at: { [Op.gt]: new Date() } }],
|
|
},
|
|
raw: true,
|
|
});
|
|
purchasers.forEach((p) => userIds.add(String(p.user_id)));
|
|
}
|
|
|
|
return [...userIds];
|
|
}
|
|
|
|
async function resolveTaskListUserIds(taskListId) {
|
|
const rows = await sequelize.query(
|
|
`SELECT DISTINCT ugm.user_id
|
|
FROM task_list_groups tlg
|
|
JOIN user_group_members ugm ON ugm.group_id = tlg.group_id AND ugm."deletedAt" IS NULL
|
|
WHERE tlg.task_list_id = :taskListId`,
|
|
{ replacements: { taskListId }, type: QueryTypes.SELECT }
|
|
);
|
|
return rows.map((r) => String(r.user_id));
|
|
}
|
|
|
|
// user_id → group_id, for deep-linking task_list broadcasts to /group/:groupId/view/:taskListId.
|
|
// First matching group wins if a user belongs to more than one group tied to the task list.
|
|
async function resolveTaskListUserGroups(taskListId) {
|
|
const rows = await sequelize.query(
|
|
`SELECT ugm.user_id, tlg.group_id
|
|
FROM task_list_groups tlg
|
|
JOIN user_group_members ugm ON ugm.group_id = tlg.group_id AND ugm."deletedAt" IS NULL
|
|
WHERE tlg.task_list_id = :taskListId`,
|
|
{ replacements: { taskListId }, type: QueryTypes.SELECT }
|
|
);
|
|
const map = {};
|
|
for (const r of rows) {
|
|
const uid = String(r.user_id);
|
|
if (!(uid in map)) map[uid] = r.group_id;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
async function resolveTierPlanUserIds(planId) {
|
|
const holders = await mdl_UserTiers.findAll({
|
|
attributes: ['user_id'],
|
|
where: { plan_id: planId, status: 'active' },
|
|
raw: true,
|
|
});
|
|
return holders.map((h) => String(h.user_id));
|
|
}
|
|
|
|
// Dispatcher for the 3 "scoped" target types only — 'admin'/'user'/'both' mean
|
|
// different things to different callers (e.g. notification broadcasts post to
|
|
// the shared admin bell feed for 'admin'; email broadcasts email every admin/
|
|
// staff user instead), so those stay caller-specific rather than living here.
|
|
async function resolveTargetUserIds(target_type, target_id) {
|
|
if (target_type === 'task_list') return resolveTaskListUserIds(target_id);
|
|
if (target_type === 'course') return resolveCourseUserIds(target_id);
|
|
if (target_type === 'tier_plan') return resolveTierPlanUserIds(target_id);
|
|
return [];
|
|
}
|
|
|
|
async function resolveAllUserIds({ transaction } = {}) {
|
|
const users = await mdl_Users.findAll({
|
|
attributes: ['user_id'],
|
|
where: { acc_type: 'user', deletedAt: null },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
return users.map((u) => String(u.user_id));
|
|
}
|
|
|
|
module.exports = {
|
|
ALLOWED_TARGET_TYPES,
|
|
SCOPED_TARGET_TYPES,
|
|
validateTargetId,
|
|
resolveCourseUserIds,
|
|
resolveTaskListUserIds,
|
|
resolveTaskListUserGroups,
|
|
resolveTierPlanUserIds,
|
|
resolveTargetUserIds,
|
|
resolveAllUserIds,
|
|
};
|