/*********************************************************************************************************************************************************************** * File Name : expire_user_tiers.cron.js * Type : Cron Job * Description : Marks active user_tiers rows as 'expired' when their expires_at * has passed. Runs every minute to support short-duration plans * (minute- and hour-level plans in addition to day/month/year). * * For each expired batch it: * 1. Bulk-updates matching rows to status = 'expired'. * 2. Sends an in-app UserNotification to each affected user. * * Safety: * - Only touches rows with expires_at IS NOT NULL so * manually-granted unlimited tiers (expires_at = NULL) are * never touched. * - Bulk update happens before notifications so a restart * mid-run never re-expires already-expired rows. * * Schedule : Every minute ("* * * * *"). Registered by cron/client.cron.js. * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 29, 2026 ***********************************************************************************************************************************************************************/ 'use strict'; const { Op } = require('sequelize'); const mdl_UserTiers = require('../../models/tiers/user_tiers.mdl'); const mdl_TierPlans = require('../../models/tiers/tier_plans.mdl'); const UserNotification = require('../../models/notifications/user_notification.mdl'); const CronNotificationSetting = require('../../models/notifications/cron_notification_setting.mdl'); const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data'); require('../../models/tiers/tier.associations'); async function run() { // ── 1. Find all active tiers whose expires_at has passed ────────────────── let expired; try { expired = await mdl_UserTiers.findAll({ where: { status: 'active', expires_at: { [Op.ne]: null, [Op.lte]: new Date() }, }, include: [{ model: mdl_TierPlans, as: 'plan', attributes: ['plan_id', 'label', 'tier'], required: false, }], attributes: ['tier_id', 'user_id', 'tier'], }); } catch (err) { console.error('[CRON][EXPIRE TIERS] Failed to query user_tiers:', err); return; } if (!expired.length) return; const tierIds = expired.map((t) => t.tier_id); // ── 2. Bulk-update to expired ────────────────────────────────────────────── try { await mdl_UserTiers.update( { status: 'expired' }, { where: { tier_id: tierIds } } ); } catch (err) { console.error('[CRON][EXPIRE TIERS] Bulk update failed:', err); return; } // ── 3. Send in-app notifications (one per affected user) ────────────────── // Status flip above always happens — only this step is skippable via settings. const settings = await CronNotificationSetting.findOne({ where: { job_name: 'expireUserTiers' } }); if (!settings || settings.enabled) { const notifications = expired.map((t) => NOTIFICATION_REGISTRY.tier_expired.build({ tier: t.tier, label: t.plan?.label ?? null, planId: t.plan?.plan_id ?? null, }) ).map((payload, i) => ({ user_id: expired[i].user_id, ...payload, })); try { await UserNotification.bulkCreate(notifications, { ignoreDuplicates: true }); } catch (err) { console.error('[CRON][EXPIRE TIERS] Notification bulkCreate failed:', err); } } console.log(`[CRON][EXPIRE TIERS] Expired ${expired.length} tier(s) for ${new Set(expired.map((t) => t.user_id)).size} user(s).`); } module.exports = { name: 'expireUserTiers', schedule: '* * * * *', run, };