client and some admin new

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-08-05 04:32:43 +08:00
parent 8922f7f2f4
commit 0c7f5ccd0f
33 changed files with 937 additions and 770 deletions
+89
View File
@@ -0,0 +1,89 @@
/***********************************************************************************************************************************************************************
* File Name: planAccess.service.js
* Type of Program: Service
* Description: Force-revokes access for every currently active subscriber of
* a Tier Plan (no refund). Always fired automatically as part of
* archivePlan/bulkArchivePlans (archiving a plan revokes its
* subscribers' access, full stop — no opt-in) and again as part
* of permanentlyDeletePlan/bulkPermanentlyDeletePlans, in case a
* plan was archived before this existed and still has active
* subscribers when it's finally deleted for good.
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Aug. 4, 2026
***********************************************************************************************************************************************************************/
'use strict';
const mdl_UserTiers = require('../models/tiers/user_tiers.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 { sendEmail } = require('./email.service');
const { fmtDate } = require('../utils/datetime.util');
// Revokes every active user_tiers row tied to `plan`, replicating revokeTier's
// per-user "auto-downgrade to Free if no other active tier remains" rule
// (controllers/admin/tiers.controller.js) — a user can hold more than one
// concurrently-active plan, so this can't be a blanket status update.
async function revokePlanSubscriberAccess(plan, revokedByUserId) {
const activeRows = await mdl_UserTiers.findAll({
where: { plan_id: plan.plan_id, status: 'active' },
attributes: ['tier_id', 'user_id'],
});
if (!activeRows.length) return { revoked_user_count: 0 };
const tierIds = activeRows.map((r) => r.tier_id);
const userIds = [...new Set(activeRows.map((r) => String(r.user_id)))];
const now = new Date();
await mdl_UserTiers.update(
{ status: 'revoked', revoked_by: revokedByUserId, revoked_at: now },
{ where: { tier_id: tierIds } },
);
for (const user_id of userIds) {
const remainingActive = await mdl_UserTiers.count({ where: { user_id, status: 'active' } });
if (remainingActive === 0) {
await mdl_UserTiers.create({
user_id,
tier: 'free',
status: 'active',
starts_at: now,
expires_at: null,
granted_by: revokedByUserId,
notes: 'Auto-downgrade after plan access was force-revoked.',
});
}
}
try {
const notify = NOTIFICATION_REGISTRY.tier_plan_access_revoked.build({ label: plan.label, planId: plan.plan_id });
await UserNotification.bulkCreate(
userIds.map((user_id) => ({ user_id, ...notify, seen: false, createdAt: now, updatedAt: now })),
{ validate: false },
);
} catch (notifyErr) {
console.error('[PLAN ACCESS REVOKE][NOTIFY]', notifyErr);
}
try {
const users = await mdl_Users.findAll({
where: { user_id: userIds },
attributes: ['user_id', 'email', 'personal_info'],
});
const dateStr = fmtDate(now);
for (const u of users) {
const name = u.personal_info?.name?.full_name ?? 'there';
sendEmail({
to: u.email,
type: 'TIER_ACCESS_REVOKED',
data: { name, label: plan.label, date: dateStr },
}).catch((emailErr) => console.error('[PLAN ACCESS REVOKE][EMAIL]', emailErr));
}
} catch (emailBatchErr) {
console.error('[PLAN ACCESS REVOKE][EMAIL BATCH]', emailBatchErr);
}
return { revoked_user_count: userIds.length };
}
module.exports = { revokePlanSubscriberAccess };
+56
View File
@@ -0,0 +1,56 @@
/***********************************************************************************************************************************************************************
* File Name: tierGrants.service.js
* Type of Program: Service
* Description: Snapshots a Tier Plan's current bundle contents (plan_courses/
* plan_units/plan_lessons — exactly one of the three per plan,
* per the single-type bundle rule) into user_tier_grants for a
* given user_tiers row. Shared by the client purchase-capture
* flow (controllers/client/tiers.controller.js#captureOrder) and
* the admin manual-grant flow (controllers/admin/tiers.controller.js#grantTier)
* so both paths produce the same item-specific entitlement.
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Aug. 4, 2026
***********************************************************************************************************************************************************************/
'use strict';
const {
mdl_PlanCourses,
mdl_PlanUnits,
mdl_PlanLessons,
mdl_UserTierGrants,
} = require('../models/tiers/tier.associations');
// Replaces every grant row tied to this user_tiers purchase with a fresh
// snapshot of the plan's current bundle — safe to call again on repeat
// purchase/extension of the same plan (refreshes to whatever the plan
// currently contains, in case an admin edited it since the first purchase).
async function snapshotPlanGrants(userTierRow, planId) {
await mdl_UserTierGrants.destroy({ where: { user_tier_id: userTierRow.tier_id } });
if (!planId) return;
const [courseRows, unitRows, lessonRows] = await Promise.all([
mdl_PlanCourses.findAll({ where: { plan_id: planId }, attributes: ['course_id'] }),
mdl_PlanUnits.findAll({ where: { plan_id: planId }, attributes: ['unit_id'] }),
mdl_PlanLessons.findAll({ where: { plan_id: planId }, attributes: ['lesson_id'] }),
]);
const grants = [
...courseRows.map((r) => ({ item_type: 'course', item_id: r.course_id })),
...unitRows.map((r) => ({ item_type: 'unit', item_id: r.unit_id })),
...lessonRows.map((r) => ({ item_type: 'lesson', item_id: r.lesson_id })),
];
if (!grants.length) return;
await mdl_UserTierGrants.bulkCreate(
grants.map((g) => ({
user_tier_id: userTierRow.tier_id,
user_id: userTierRow.user_id,
plan_id: planId,
item_type: g.item_type,
item_id: g.item_id,
granted_at: new Date(),
})),
);
}
module.exports = { snapshotPlanGrants };