/*********************************************************************************************************************************************************************** * 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 any of `plans`, 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. Batched into flat, count-independent queries (no per-user or // per-plan loop hitting the DB) so this scales to any number of affected // plans/subscribers in a fixed number of round trips. async function revokePlanSubscriberAccessBulk(plans, revokedByUserId) { if (!plans.length) return { revoked_user_count: 0 }; const planIds = plans.map((p) => p.plan_id); const labelByPlanId = new Map(plans.map((p) => [String(p.plan_id), p.label])); const activeRows = await mdl_UserTiers.findAll({ where: { plan_id: planIds, status: 'active' }, attributes: ['tier_id', 'user_id', 'plan_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 } }, ); // One grouped query replaces a per-user COUNT: finds everyone who still // holds another active tier after the revoke above. const stillActiveRows = await mdl_UserTiers.findAll({ where: { user_id: userIds, status: 'active' }, attributes: ['user_id'], group: ['user_id'], }); const stillActiveUserIds = new Set(stillActiveRows.map((r) => String(r.user_id))); const usersToDowngrade = userIds.filter((user_id) => !stillActiveUserIds.has(user_id)); if (usersToDowngrade.length) { await mdl_UserTiers.bulkCreate( usersToDowngrade.map((user_id) => ({ user_id, tier: 'free', status: 'active', starts_at: now, expires_at: null, granted_by: revokedByUserId, notes: 'Auto-downgrade after plan access was force-revoked.', })), ); } // One row per (user, plan) relationship revoked — a user in two of the // selected plans gets two notices/emails, one per plan label. const revokedPairs = [...new Map(activeRows.map((r) => [`${r.user_id}:${r.plan_id}`, r])).values()]; try { const notifications = revokedPairs.map((r) => { const notify = NOTIFICATION_REGISTRY.tier_plan_access_revoked.build({ label: labelByPlanId.get(String(r.plan_id)), planId: r.plan_id, }); return { user_id: String(r.user_id), ...notify, seen: false, createdAt: now, updatedAt: now }; }); await UserNotification.bulkCreate(notifications, { 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 usersById = new Map(users.map((u) => [String(u.user_id), u])); const dateStr = fmtDate(now); for (const r of revokedPairs) { const u = usersById.get(String(r.user_id)); if (!u) continue; const name = u.personal_info?.name?.full_name ?? 'there'; sendEmail({ to: u.email, type: 'TIER_ACCESS_REVOKED', data: { name, label: labelByPlanId.get(String(r.plan_id)), 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 }; } async function revokePlanSubscriberAccess(plan, revokedByUserId) { return revokePlanSubscriberAccessBulk([plan], revokedByUserId); } module.exports = { revokePlanSubscriberAccess, revokePlanSubscriberAccessBulk };